Search code examples
unit-testingjestjsnestjsts-jestnestjs-typeorm

How to mock/test NestJS service calls to repository?


I have a NestJS service that has a pretty simple method I am trying to write unit tests for

@Injectable()
export class TestService {

  constructor(
    @InjectRepository(TestEntity)
    private readonly repo: Repository<TestEntity>,
  ) {}

  async getAllTests(acct: AccountEntity): Promise<Array<TestEntity>> {
    try {
      const query = this.repo.createQueryBuilder(DB_TABLE_NAME_TEST);
      query.where({ account: acct.id });

      return await query.getMany();
    } catch (ex) {
      throw new InternalServerErrorException();
    }
  }
}

And the tests

const mockAccount: AccountEntity = {
  id: 'mock-account-id-1234',
  email: '[email protected]',
};

const mockTest: TestEntity = {
  id: '1234-5678-9012',
  account: 'mock-account-id-1234',
  description: 'Test Name',
  testerName: 'Bob Jones',
  testerEmail: '[email protected]',
};

describe('TestService', () => {
  let service: TestService;
  let repo: Repository<TestEntity>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        TestService,
        {
          provide: getRepositoryToken(TestEntity),
          useValue: {
            create: jest.fn(),
            save: jest.fn(),
            delete: jest.fn(),
            createQueryBuilder: jest.fn(() => ({
              where: jest.fn(),
              getMany: jest.fn(),
              getOne: jest.fn(),
            })),
          },
        },
      ],
    }).compile();

    service = module.get(TestService);
    repo = module.get(getRepositoryToken(TestEntity));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('getAllTests()', () => {
    it('should successfully get a list of all tests for the provided account', async () => {
      jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'where')
      jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockResolvedValue([mockTest]);

      expect(repo.createQueryBuilder(DB_TABLE_NAME_TEST).where).toHaveBeenCalledWith({ account: mockAccount.id });
      await expect(service.getAllTests(mockAccount)).resolves.toEqual([mockTest]);
    });

    it('should throw an error when the query fails', async () => {
      jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'where').mockImplementation(() => {
        throw new Error();
      });

      jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockImplementation(() => {
        throw new Error();
      });

      const result = () => service.getAllTests(mockAccount);
      await expect(result).rejects.toThrow(InternalServerErrorException);
    });
  });
});

Both of those tests for the getAllTests() method fails with:

 FAIL  src/test/test.service.spec.ts
  ● TestService › getAllTests() › should successfully get a list of all tests for the provided account

    expect(jest.fn()).toHaveBeenCalledWith(...expected)

    Expected: {"account": "mock-account-id-1234"}

    Number of calls: 0

      143 |       jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockResolvedValue([mockTest]);
      144 |
    > 145 |       expect(repo.createQueryBuilder(DB_TABLE_NAME_TEST).where).toHaveBeenCalledWith({ account: mockAccount.id });
          |                                                                 ^
      146 |       await expect(service.getAllTests(mockAccount)).resolves.toEqual([mockTest]);
      147 |     });
      148 |

      at Object.<anonymous> (test/test.service.spec.ts:145:65)

  ● TestService › getAllTests() › should throw an error when the query fails

    expect(received).rejects.toThrow()

    Received promise resolved instead of rejected
    Resolved to value: undefined

      157 |
      158 |       const result = () => service.getAllTests(mockAccount);
    > 159 |       await expect(result).rejects.toThrow(InternalServerErrorException);
          |             ^
      160 |     });
      161 |   });
      162 | });

      at expect (../node_modules/expect/build/index.js:113:15)
      at Object.<anonymous> (test/test.service.spec.ts:159:13)

This seems like it should be pretty straight forward. What am I doing wrong here?


Solution

  • In your first test, you assert the mock.where to have been called before calling the service. Also every time you call createQueryBuilder, you'll get back a new .where with a new jest.fn() tracking the calls to it. What I would suggest you do here is create a separate const whereMock = jest.fn().mockReturnThis() so you can set where: whereMock in the createQueryBuilder object. This will allow you to access the correct jest.fn() when asserting what the where has been called with

    And in the second test, once again because the createQueryBuilder is a jest.fn() every call to it returns a new object, so your jest.spyOn calls are not spying on the same object that the service is using. To get past this, you can create mocks like I suggested for the whereMock so they can be referenced directly without having to go through the new object.