Search code examples
typescriptjestjsmockingnestjsnestjs-testing

Jest's .toHaveBeenCalledWith() assertion doesn't work with injected mock repo in NestJS TestingModule


I'm writing tests for my users.service file. To test the update method, I wanted to check if the user repository's persistAndFlush() method is called with the right data.

users.service.ts

import { InjectRepository } from '@mikro-orm/nestjs';
import { EntityRepository } from '@mikro-orm/sqlite';
import { Injectable, NotFoundException } from '@nestjs/common';
import { UserDto } from './dto/user.dto';
import { User } from './entities/user';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private userRepo: EntityRepository<User>,
  ) {}

//...

  async update(id: number, userData: UserDto): Promise<User> {
    const user = await this.userRepo.findOne({ id });
    if (!user) {
      throw new NotFoundException();
    }
    Object.assign(user, userData);

    await this.userRepo.persistAndFlush(user);
    return user;
  }
}

user-repo.mock.ts

export function mockUserRepo() {
  return {
    findOne: jest.fn(),
    persistAndFlush: jest.fn(() => {
      return undefined;
    }),
  };
}

users.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { getRepositoryToken } from '@mikro-orm/nestjs';
import { faker } from '@mikro-orm/seeder';
import { mockUserRepo } from './mocks/user-repo.mock';
import { UsersService } from './users.service';
import { UserDto } from './dto/user.dto';
import { User } from './entities/user';

describe('UsersService', () => {
  let service: UsersService;

  const mockRepo = mockUserRepo();

  beforeEach(async () => {

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepo },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
  });

  //...

  it('should update database and return updated value when updating valid user', () => {
    const regDate = new Date();
    regDate.setFullYear(new Date().getFullYear() - 1);
    const user1 = new User({
      id: 1,
      userName: faker.internet.userName(),
      email: faker.internet.email(),
      password: faker.internet.password(),
      registeredAt: regDate,
      lastLogin: new Date(),
      isAdmin: 'false',
    });

    const userDto = new UserDto();
    userDto.email = `updated.${user1.email}`;

    const expectedUser = new User({
      id: 1,
      userName: user1.userName,
      email: userDto.email,
      password: user1.password,
      registeredAt: user1.registeredAt,
      lastLogin: user1.lastLogin,
      isAdmin: 'false',
    });

    mockRepo.findOne.mockImplementationOnce((inObj: any) => {
      if (inObj.id === user1.id) return user1;
      return undefined;
    });

    expect(service.update(1, userDto)).resolves.toEqual(expectedUser);
    expect(mockRepo.persistAndFlush).toHaveBeenCalledWith(expectedUser);
  });
}

output of $ npm jest user.service.spec.ts

  ● UsersService › should update database and return updated value when updating valid user

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

    Expected: {"email": "[email protected]", "id": 1, "isAdmin": "false", "lastLogin": 2022-11-19T16:24:56.702Z, "password": "tbV1cCtYILwjrLI", "registeredAt": 2021-11-19T16:24:56.702Z, "userName": "Manuel78"}

    Number of calls: 0

      137 |
      138 |     expect(service.update(1, userDto)).resolves.toEqual(expectedUser);
    > 139 |     expect(mockRepo.persistAndFlush).toHaveBeenCalledWith(expectedUser);
          |                                      ^
      140 |   });
      141 |
      142 |   /**

      at Object.<anonymous> (users/users.service.spec.ts:139:38)

Env:
Ubuntu 22.04.1 LTS
Node 18.12.1
nestjs ^9.0.0
jest 28.1.3
ts-jest 28.0.8
typescript ^4.7.4
ts-node ^10.0.0

Troubleshooting steps:

  • tried defining the mock repo locally in the .spec.ts file
  • tried instantiating mockRepo inside the beforeEach() call
  • tried overriding the default mockrepo.persistAndFlush() implementation with a custom one using .mockImpelementation() inside the callback in it()
  • also tried using .mockResolvedValue() (this was actually the initial version)

Is this a bug in the Jest integration, or am I missing some encapsulation issue or something?

If you can suggest another way to validate this without using the .toHaveBeenCalledWith() method, that's fine as well. I thought about putting the result of the call into the return value but tbh, I do not want to change the signature of the update() method. It would be a major pain in the rear end.

Thanks in advance!


Solution

  • The issue has been asynchronicity. I looked at this example for the controller tests and I realized that even though the mock did not always have an async implementation, the methods that I tried to test did. So I tried it and now it makes total sense.

    Given the function foo(): Promise<any>, the expect(foo()).resolves.toEqual() call does not block, so the subsequent expect(...).toHaveBeenCalledWith(...) call can be executed before the promise returned by foo() is resolved.

    The solution is to define the callback like this:

    it('should...', async () => {
      // ...
      expect(await foo()).toEqual(expectedResult);
      expect(someFunctionCalledInsideFoo).toHaveBeenCalledWith(expectedParams);
    });