14

API with NestJS #9. Testing services and controllers with integration tests

 4 years ago
source link: https://wanago.io/2020/07/13/api-nestjs-testing-services-controllers-integration-tests/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

In the previous part of this series , we’ve focused on unit tests. This time, we look into  integration tests . In this article, we explain their principles and how they differ from unit tests. We write a few of them using Jest to test our services. We also look into the SuperTest library to test our controllers.

Testing NestJS services with integration tests

When our unit tests pass, it indicates that parts of our system work well on their own. However, an application consists of many parts that should work well together. A job of an integration test is to verify that all the cogs in the wheel integrate. We can write such tests integrating two or more parts of the system.

Let’s test how AuthenticationService integrates with UsersService .

src/authentication/tests/authentication.service.spec.ts

import { AuthenticationService } from '../authentication.service';
import { Test } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { getRepositoryToken } from '@nestjs/typeorm';
import User from '../../users/user.entity';
import { UsersService } from '../../users/users.service';
import mockedJwtService from '../../utils/mocks/jwt.service';
import mockedConfigService from '../../utils/mocks/config.service';
 
describe('The AuthenticationService', () => {
  let authenticationService: AuthenticationService;
  let usersService: UsersService;
  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        UsersService,
        AuthenticationService,
        {
          provide: ConfigService,
          useValue: mockedConfigService
        },
        {
          provide: JwtService,
          useValue: mockedJwtService
        },
        {
          provide: getRepositoryToken(User),
          useValue: {}
        }
      ],
    })
      .compile();
    authenticationService = await module.get(AuthenticationService);
    usersService = await module.get(UsersService);
  })
  describe('when accessing the data of authenticating user', async () => {
    it('should attempt to get the user by email', () => {
      const getByEmailSpy = jest.spyOn(usersService, 'getByEmail');
      await authenticationService.getAuthenticatedUser('[email protected]', 'strongPassword');
      expect(getByEmailSpy).toBeCalledTimes(1);
    })
  })
});

The first thing to notice above is that we mock some of the services that we use. Even though we want to write an integration test, it does not mean that we need to include every part of the system.

Mocking some parts of the system

We need to decide how many parts of the system we want to include. Let’s assume that we want to test the integration of the AuthenticationService and  UsersService . Going further, let’s also mock the bcrypt library.

Since our AuthenticationService directly imports it, it is not straightforward to mock. To do that, we need to use  jest . mock .

jest.mock('bcrypt');

Now that we explicitly state that we mock bcrypt, we can provide our implementation of it.

import * as bcrypt from 'bcrypt';
 
describe('The AuthenticationService', () => {
  let bcryptCompare: jest.Mock;
  beforeEach(async () => {
    bcryptCompare = jest.fn().mockReturnValue(true);
    (bcrypt.compare as jest.Mock) = bcryptCompare;
  });
});

Thanks to declaring bcryptCompare at the top, we can now change its implementation for each test.

We do a similar thing for the repository.

import User from '../../users/user.entity';
 
const mockedUser: User = {
  id: 1,
  email: '[email protected]',
  name: 'John',
  password: 'hash',
  address: {
    id: 1,
    street: 'streetName',
    city: 'cityName',
    country: 'countryName'
  }
}
import User from '../../users/user.entity';
import * as bcrypt from 'bcrypt';
import mockedUser from './user.mock';
 
jest.mock('bcrypt');
 
describe('The AuthenticationService', () => {
  let bcryptCompare: jest.Mock;
  let userData: User;
  let findUser: jest.Mock;
 
  beforeEach(async () => {
    bcryptCompare = jest.fn().mockReturnValue(true);
    (bcrypt.compare as jest.Mock) = bcryptCompare;
 
    userData = {
      ...mockedUser
    }
    findUser = jest.fn().mockResolvedValue(userData);
    const usersRepository = {
      findOne: findUser
    }
  })
});

Providing different implementations per test

Once we do all of the above, we can provide different implementations of our mocked services for various tests.

describe('when accessing the data of authenticating user', () => {
  describe('and the provided password is not valid', () => {
    beforeEach(() => {
      bcryptCompare.mockReturnValue(false);
    });
    it('should throw an error', async () => {
      await expect(
        authenticationService.getAuthenticatedUser('[email protected]', 'strongPassword')
      ).rejects.toThrow();
    })
  })
  describe('and the provided password is valid', () => {
    beforeEach(() => {
      bcryptCompare.mockReturnValue(true);
    });
    describe('and the user is found in the database', () => {
      beforeEach(() => {
        findUser.mockResolvedValue(userData);
      })
      it('should return the user data', async () => {
        const user = await authenticationService.getAuthenticatedUser('[email protected]', 'strongPassword');
        expect(user).toBe(userData);
      })
    })
    describe('and the user is not found in the database', () => {
      beforeEach(() => {
        findUser.mockResolvedValue(undefined);
      })
      it('should throw an error', async () => {
        await expect(
          authenticationService.getAuthenticatedUser('[email protected]', 'strongPassword')
        ).rejects.toThrow();
      })
    })
  })
})

Above, we specify how our mocks work in the beforeEach functions. Thanks to doing that, it would run before all the tests in a particular describe ( ) block.

Check out this file in the repository , if you want to inspect the above test suite thoroughly.

Testing controllers

We perform another type of integration tests by performing real requests. By doing so, we can test our controllers. It is closer to how our application is used. To do so, we use the SuperTest library.

npm install supertest

Now, let’s test how the AuthenticationController integrates with  AuthenticationService and  UsersService .

We start by mocking some of the parts of the application.

let app: INestApplication;
let userData: User;
beforeEach(async () => {
  userData = {
    ...mockedUser
  }
  const usersRepository = {
    create: jest.fn().mockResolvedValue(userData),
    save: jest.fn().mockReturnValue(Promise.resolve())
  }
 
  const module = await Test.createTestingModule({
    controllers: [AuthenticationController],
    providers: [
      UsersService,
      AuthenticationService,
      {
        provide: ConfigService,
        useValue: mockedConfigService
      },
      {
        provide: JwtService,
        useValue: mockedJwtService
      },
      {
        provide: getRepositoryToken(User),
        useValue: usersRepository
      }
    ],
  })
    .compile();
  app = module.createNestApplication();
  app.useGlobalPipes(new ValidationPipe());
  await app.init();
})

Please notice that above we also need to apply the ValidationPipe if we want to verify our validation.

Once we have our module ready, we can perform some tests on it. Let’s start with the registration flow.

describe('when registering', () => {
  describe('and using valid data', () => {
    it('should respond with the data of the user without the password', () => {
      const expectedData = {
        ...userData
      }
      delete expectedData.password;
      return request(app.getHttpServer())
        .post('/authentication/register')
        .send({
          email: mockedUser.email,
          name: mockedUser.name,
          password: 'strongPassword'
        })
        .expect(201)
        .expect(expectedData);
    })
  })
  describe('and using invalid data', () => {
    it('should throw an error', () => {
      return request(app.getHttpServer())
        .post('/authentication/register')
        .send({
          name: mockedUser.name
        })
        .expect(400)
    })
  })
})

Above, we perform real HTTP requests and test the authentication / register endpoint. If we provide valid data, we expect it to work correctly. Otherwise, we expect it to throw an error.

Aside from simple tests like those above, we can perform more throughout ones. For example, we can verify the response headers. For a full list of SuperTest features, check out the documentation .

To see the whole controller test suite, check it out in the repository .

Summary

In this article, we’ve gone through ways to write integration tests for our NestJS API. Aside from testing how our services integrate, we’ve also used the SuperTest library and tested a controller. By writing integration tests, we can thoroughly verify if our app works as expected. Therefore, it is a topic worth diving into.

Series Navigation

<< API with NestJS #8. Writing unit tests


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK