typescriptjestjstypeorm

How to Mock TypeORM's Entity Manager Transaction for behavior assertion when Unit Testing in Jest?


Question

I am trying to write a unit test for a method that uses TypeORM's entity manager transaction. The method is defined as follows:

class MyRepository {

  private readonly repository: Repository<MyEntity>;

  myFunction() {
    this.repository.manager.transaction(async (trx: EntityManager) => {
      await trx.delete(MyEntity, { id: "123" });
    });
  }
}

I want to somehow mock the transaction so that I can assert that the delete method is called within the transaction. Something like this:

const repository: jest.Mocked<Repository<MyEntity>;

...

describe("when calling myFunction", () => {
  it("should call the delete method on the transaction", async () => {
    await myRepository.myFunction();
    expect(deleteMock).toHaveBeenCalled();
  });
});

However, I am unsure how to properly mock the transaction and the delete method. How can I achieve this using Jest? Any guidance or examples would be greatly appreciated!

What have I tried already?

I've already tried to mock the Repository:

const deleteMock = jest.fn();

function mockRepository<E extends Object>(): jest.Mocked<Repository<E>> {
  return {
    ...
    manager: {
      transaction: jest.fn().mockReturnValue({
        delete: deleteMock,
      }),
    } as jest.Mocked<Partial<EntityManager>> as jest.Mocked<EntityManager>,
  } as jest.Mocked<Partial<Repository<E>>> as jest.Mocked<Repository<E>>;
}

And checking the manager's transaction method is called works:

describe("when calling myFunction", () => {
  it("should call initialize a transaction", async () => {
    await myRepository.myFunction();
    expect(repository.manager.transaction).toHaveBeenCalled();
  });
});

However, as you can image, this is not useful as a test, since I'm not checking what's being done inside the transaction.

When trying to assert the call for deleteMock (expect(deleteMock).toHaveBeenCalled()), the test fails, as if the method was never called.


Solution

  • After further research, I found out how to solve the issue thanks to this response from Adams.

    Basically, I had to call the method that initialized the transaction, then, I had to call the callback function that was provided by the code.

    From then, all I had to do was mock the EntityManager passed down to the argument defined on the code.

    For the mocked repository, notice how we don't care what the transaction implementation or response is:

    function mockRepository<E extends Object>(): jest.Mocked<Repository<E>> {
      return {
        ...
        manager: {
          transaction: jest.fn(),
        } as jest.Mocked<Partial<EntityManager>> as jest.Mocked<EntityManager>,
      } as jest.Mocked<Partial<Repository<E>>> as jest.Mocked<Repository<E>>;
    }
    

    After that, I created a definition for the mocked transaction that I use on the test it self later. I had to create the custom type TransactionMock since the EntityManager["transaction"] method is overloaded with multiple implementations, and we only care about the one without the _IsolationLevel_:

    const transactionMock = {
      delete: jest.fn(),
      save: jest.fn(),
      find: jest.fn(),
    } as jest.Mocked<Partial<EntityManager>> as jest.Mocked<EntityManager>;
    
    type TransactionMock = Parameters<jest.Mocked<EntityManager["transaction"]>>[1];
    
    function getMockedTransaction<E extends Object>(
      mockedRepository: jest.Mocked<Repository<E>>,
    ): TransactionMock {
      return (mockedRepository.manager.transaction as jest.Mock)
        .mock.calls[0][0];
    }
    

    The example of the test using the mock is the following:

    const repository = mockRepository<MyEntity>();
    const myRepository = new MyRepository(repository);
    describe("when calling myFunction", () => {
    
        it("should initialize the transaction", async () => {
          await myRepository.myFunction();
    
          expect(repository.manager.transaction).toHaveBeenCalled();
        });
    
        it("should delete the entity", async () => {
    
          await myRepository.myFunction();
          const trx = getMockedTransaction(repository);
          await trx(transactionMock);
    
          expect(transactionMock.delete).toHaveBeenCalledWith(MyEntity, {
            id: "123"
          });
        });
    })