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

栏目: IT技术 · 发布时间: 4年前

内容简介:In theWhen ourLet’s test how

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('user@email.com', '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: 'user@email.com',
  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('user@email.com', '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('user@email.com', '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('user@email.com', '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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Black Box Society

The Black Box Society

Frank Pasquale / Harvard University Press / 2015-1-5 / USD 35.00

Every day, corporations are connecting the dots about our personal behavior—silently scrutinizing clues left behind by our work habits and Internet use. The data compiled and portraits created are inc......一起来看看 《The Black Box Society》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具