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!
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>
, theexpect(foo()).resolves.toEqual()
call does not block, so the subsequentexpect(...).toHaveBeenCalledWith(...)
call can be executed before the promise returned byfoo()
is resolved.The solution is to define the callback like this: