Jest testing with NestJS
If you haven’t heard about NestJS, wait no longer! Explore it here. This is a great NodeJS framework inspired by Angular and Spring. It is already set up and ready to go right out of the box. Generally speaking, Nest’s authors did a great job. But when it comes to real application testing it isn’t straight forward to work out how to use it. In this article, I will demonstrate how I prefer to approach problems with mocking and test data.
Real application setup
NestJS is built, just like Angular, in a modular way in which modules consist mainly of Controllers, Gateways, and Services. Let’s consider a simple architecture with a controller, each using two services:
some-api.service.ts
@Injectable()
export class SomeAPIService {
constructor() {}
public getCars(): Promise {
return this.sendRequest({ url: '/cars' });
}
public postCar(car: any): Promise {
return this.sendRequest({
url: '/cars',
method: 'post',
form: { car },
});
}
...
}
cars.service.ts
@Injectable()
export class CarsService {
constructor(private readonly someAPIService: SomeAPIService) {}
public cars(): Promise<any[]> {
return this.someAPIService.getCars();
}
public async addCar(car: any): Promise {
try {
return await this.someAPIService.postCar(car);
} catch (e) {
return { error: `Failed to add car` };
}
}
}
app.controller.ts
@Controller()
export class AppController {
constructor(private readonly carsService: CarsService) {}
@Get()
async index(): Promise<any[]> {
return await this.carsService.cars();
}
@Post()
async create(@Body() body): Promise<any[]> {
if (!body.car) {
throw new HttpException(`missing car in request`, HttpStatus.UNPROCESSABLE_ENTITY);
}
return await this.carsService.addCar(body.car);
}
}
Unit testing
At this stage, let’s create a unit test for all pieces (I’ll skip over my theory on what unit tests are about for the time being). We need to have specifications for all public methods for our services and controllers, but each service needs to be tested separately. Therefore, we will create mocks for everything that is outside of the tested service/controller. Additionally, we need to define the factory data outside of the test so the code appears more legible and pleasant to the eye.
cars.service.spec.ts
import { SomeAPIService } from './some-api.service';
jest.mock('./some-api.service');
describe('CarsService', () => {
let service: CarsService, apiService: SomeAPIService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CarsService,
SomeAPIService,
],
}).compile();
service = module.get(CarsService);
apiService = module.get(SomeAPIService);
});
it('should respond to cars', async () => {
const cars = CarFactory.buildList(10)
jest.spyOn(apiService, 'getCars').mockResolvedValue(cars);
expect(await service.cars()).toEqual(cars);
});
it('should add cars to API', async () => {
const car = CarFactory.build();
jest.spyOn(apiService, 'postCar').mockResolvedValue(car);
expect(await service.addCar(CarFactory.build())).toEqual(car);
});
it('should throw error when adding cars to API', async () => {
jest.spyOn(apiService, 'postCar').mockRejectedValue(null)
expect(await service.addCar(CarFactory.build())).toEqual({ error: `Failed to add car` });
});
});
test/factories/car.ts
export class CarFactory {
make: 'Audi';
model: 'A4';
public static build(opts: any = {}) {
return new CarFactory(opts);
}
public static buildList(length: number, opts: any = {}) {
return Array.apply(null, { length }).map(() => this.build(opts));
}
constructor(opts: any = {}) {
Object.assign(this, opts);
}
}
We need to mock everything that needn’t be covered in this test as it should be done in another spec (unit tests – duh). By doing
jest.mock('./some-api.service');
we are saying that everything in the file some-api.service.ts
is mocked. In result, class SomeAPIService
gets a brand new body with all methods doing and returning nothing. Consequently, in each test case, we’re mocking any method we desire.
Additionally, for the sake of code cleanliness, I have introduced CarFactory
class which simply delegates the creation of sample data outside test cases. The result is that we have clean unit tests without sample data blended into it.
Testing controller
The testing controller struck me as a little tricky because of all the decorators that are applied to method arguments. This is not e2e but a unit test, so we’re not checking whether the decorators work, but rather if any function of this controller does what it is supposed to do when passing a particular set of arguments and conditions.
import { CarsService } from './services/cars.service';
jest.mock('./services/cars.service');
describe('AppController', () => {
let module: TestingModule, carsService: CarsService;
beforeAll(async () => {
module = await Test.createTestingModule({
controllers: [AppController],
providers: [
CarsService,
],
}).compile();
});
beforeEach(() => {
carsService = module.get(CarsService);
});
describe('index', () => {
it('should return list of cars', async () => {
const cars = CarFactory.buildList(10);
jest.spyOn(carsService, 'cars').mockResolvedValue(cars);
const appController = module.get(AppController);
expect(await appController.index()).toBe(cars);
});
});
describe('create', () => {
it('should add car', async () => {
const car = CarFactory.build();
const appController = module.get(AppController);
jest.spyOn(carsService, 'addCar').mockImplementation((input) => input);
expect(await appController.create({ car })).toBe(car);
});
it('throw error if passing empty car', (done) => {
const car = CarFactory.build();
const appController = module.get(AppController);
jest.spyOn(carsService, 'addCar').mockImplementation((input) => input);
appController.create({})
.then(() => done.fail(`AppController.create din't throw error on empty car`))
.catch((error) => {
expect(error.status).toBe(422);
expect(error.message).toBe(`missing car in request`);
done();
});
});
});
});
This test looks completely normal, but one aspect is very important to remember when testing the Nest controller. If you want to return an error (404, 422, forbidden) then you’re throwing exceptions inside these methods. Here in the it function you can use the done object. If we use that, then we MUST be sure we call done()
or done.fail(`
or jest will simply exit after a pre-defined timeout (5s by default).
But wait! Jest has a toThrow matcher to solve these issues. In most cases, controller methods will be async functions which are functions returning promise so no exception will be given – ever. We could use build-in structure:
expect(appController.create({ car })).rejects(HttpException)
However, if create won’t reveal any errors, Jest will report it in an unreadable form (I won’t waste space posting sample responses here). We can also check if the correct exception class was given but not what’s inside.
Another approach I found was:
try {
await appController.create({ car });
} catch (error) {
expect(error.status).toBe(422);
expect(error.message).toBe(`missing car in request`);
}
Unfortunately, it has a major problem – if create won’t show any errors the test will pass o_O
Why? Because, if
await appController.create({ car })
doesn’t show anything, No Expect will be called, and if No Expect is called then the test will be successful and return no errors.
A quick solution would be to put
expect(`It should throw`).toBe(`While it didn't`);
after calling create but it’s not elegant.
E2E tests
NestJS team has also prepared setup for “e2e” tests (if this is the API for Angular then it’s not quite e2e but this is subject for discussion during a boring conference ;) ) With this you can do real requests to your API while stubbing (not mocking) requests to external services that you don’t want to be requested.
app.e2e-test.ts
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(SomeAPIService)
.useClass(SomeAPIServiceMock)
.compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', async () => {
SomeAPIServiceMock.setCarsResponse(CarFactory.buildList(10));
const response = await request(app.getHttpServer())
.get('/')
.expect(200);
expect(response.body).toEqual(SomeAPIServiceMock.cars);
});
it('/ (POST) calls API to insert car', async () => {
const length = SomeAPIServiceMock.cars.length;
await request(app.getHttpServer())
.post('/')
.send({ car: CarFactory.build() })
.expect(201);
expect(SomeAPIServiceMock.cars.length - length).toBe(1);
});
it('/ (POST) without params should throw an error', async () => {
const response = await request(app.getHttpServer())
.post('/')
.expect(422);
expect(response.body.message).toBe(`missing car in request`);
});
});
In a similar way to the unit test, I want to mock the implementation of service by making requests to the external API. I have achieved that by calling overrideProvider
and introducing the replacement class SomeAPIServiceMock
. Thanks to this, in each test case we can define what should be returned from the external API and check the expected result.
export class SomeAPIServiceMock {
private static carsP = [];
static setCarsResponse(cars) {
this.carsP = cars;
}
public static get cars() {
return this.carsP;
}
public async getCars(): Promise {
return SomeAPIServiceMock.cars;
}
public async postCar(car: any): Promise {
SomeAPIServiceMock.cars.push(car);
return car;
}
}
Conclusion
I hope this article will help you write better tests for your Node NestJS application and that these large chunks of code will give you a better understanding of how to write tests with mocked dependencies and also how to prepare test data.