homeresume
 
   
🔍

Testing custom repositories (NestJS/TypeORM)

September 5, 2021

Custom repositories extend the base repository class and enrich it with several additional methods. This post covers the unit and integration testing.

// user.repository.ts
@Injectable()
export class UserRepository extends Repository<UserEntity> {
constructor(private dataSource: DataSource) {
super(UserEntity, dataSource.createEntityManager());
}
async getById(id: string) {
return this.findOne({ where: { id } });
}
// ...
}

Setup

Inject a custom repository into the service.

// user.service.ts
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async getById(id: string): Promise<User> {
return this.userRepository.getById(id);
}
// ...
}

Pass entity class to the forFeature method.

// user.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([UserEntity])],
// ...
],
providers: [UserService, UserRepository],
// ...
})
export class UserModule {}

Unit testing

To properly unit-test the custom repository, mock some methods.

// user.repository.spec.ts
describe('UserRepository', () => {
let userRepository: UserRepository;
const dataSource = {
createEntityManager: jest.fn()
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserRepository,
{
provide: DataSource,
useValue: dataSource
}
]
}).compile();
userRepository = module.get<UserRepository>(UserRepository);
});
describe('getById', () => {
it('should return found user', async () => {
const id = 'id';
const user = {
id
};
const findOneSpy = jest
.spyOn(userRepository, 'findOne')
.mockResolvedValue(user as UserEntity);
const foundUser = await userRepository.getById(id);
expect(foundUser).toEqual(user);
expect(findOneSpy).toHaveBeenCalledWith({ where: user });
});
});
});

Integration testing

Integration testing is more suitable when working with databases. Read more about it on Integration testing Node.js apps post

Boilerplate

Here is the link to the boilerplate I use for the development. It contains the examples mentioned above with more details.

Spies and mocking with Jest

August 19, 2021

Besides asserting the output of the function call, unit testing includes the usage of spies and mocking. Spies are functions that let you spy on the behavior of functions called indirectly by some other code. Spy can be created by using jest.fn(). Mocking injects test values into the code during the tests. Some of the use cases will be presented below.

  • Async function and its resolved value can be mocked using mockResolvedValue. Another way to mock it is by using mockImplementation and providing a function as an argument.
const calculationService = {
calculate: jest.fn()
};
jest.spyOn(calculationService, 'calculate').mockResolvedValue(value);
jest
.spyOn(calculationService, 'calculate')
.mockImplementation(async (a) => Promise.resolve(a));
  • Rejected async function can be mocked using mockRejectedValue and mockImplementation.
jest
.spyOn(calculationService, 'calculate')
.mockRejectedValue(new Error(errorMessage));
jest
.spyOn(calculationService, 'calculate')
.mockImplementation(async () => Promise.reject(new Error(errorMessage)));
await expect(calculateSomething(calculationService)).rejects.toThrowError(
Error
);
  • Sync function and its return value can be mocked using mockReturnValue and mockImplementation.
jest.spyOn(calculationService, 'calculate').mockReturnValue(value);
jest.spyOn(calculationService, 'calculate').mockImplementation((a) => a);
  • Chained methods can be mocked using mockReturnThis.
// calculationService.get().calculate();
jest.spyOn(calculationService, 'get').mockReturnThis();
  • Async and sync functions called multiple times can be mocked with different values using mockResolvedValueOnce and mockReturnValueOnce, respectively, and mockImplementationOnce.
jest
.spyOn(calculationService, 'calculate')
.mockResolvedValueOnce(value)
.mockResolvedValueOnce(otherValue);
jest
.spyOn(calculationService, 'calculate')
.mockReturnValueOnce(value)
.mockReturnValueOnce(otherValue);
jest
.spyOn(calculationService, 'calculate')
.mockImplementationOnce((a) => a + 3)
.mockImplementationOnce((a) => a + 5);
  • External modules can be mocked similarly to spies. For the following example, let's suppose axios package is already used in one function. The following example represents a test file where axios is mocked using jest.mock().
import axios from 'axios';
jest.mock('axios');
// within test case
axios.get.mockResolvedValue(data);
  • Manual mocks are resolved by writing corresponding modules in __mocks__ directory, e.g., fs/promises mock will be stored in __mocks__/fs/promises.js file. fs/promises mock will be resolved using jest.mock() in the test file.
jest.mock('fs/promises');
  • To assert called arguments for a mocked function, an assertion can be done using toHaveBeenCalledWith matcher.
const spy = jest.spyOn(calculationService, 'calculate');
expect(spy).toHaveBeenCalledWith(firstArgument, secondArgument);
  • To assert skipped call for a mocked function, an assertion can be done using not.toHaveBeenCalled matcher.
const spy = jest.spyOn(calculationService, 'calculate');
expect(spy).not.toHaveBeenCalled();
  • To assert how many times mocked function is called, an assertion can be done using toHaveBeenCalledTimes matcher.
const spy = jest.spyOn(calculationService, 'calculate');
calculationService.calculate(3);
calculationService.calculate(2);
expect(spy).toHaveBeenCalledTimes(2);
  • To assert called arguments for the exact call when a mocked function is called multiple times, an assertion can be done using toHaveBeenNthCalledWith matcher.
const argumentsList = [0, 1];
argumentsList.forEach((argument, index) => {
expect(calculationService.calculate).toHaveBeenNthCalledWith(
index + 1,
argument
);
});
  • Methods should be restored to their initial implementation before each test case.
// package.json
"jest": {
// ...
"restoreMocks": true
}
// ...

Demo

The demo with the mentioned examples is available here.