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:
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>
, 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);
});