Nestjs单元测试分享

1,137 阅读4分钟

背景

"Test Pyramid"

越往上成本越高、速度越慢

"the Testing Trophy"

强调了集成测试的重要性

reference:noriste.github.io/reactjsday-…

Node.js module system

CommonJS modules

// foo.js
const circle = require('./circle.js'); 
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);

// circle.js
const { PI } = Math; exports.area = (r) => PI * r ** 2; exports.circumference = (r) => 2 * PI * r;

ECMAScript modules

// addTwo.js

function addTwo(num) {   
  return num + 2; 
} export { addTwo };

// other file
import { addTwo } from './addTwo.js'; // Prints: 6 console.log(addTwo(4));

模块化设计要素:

  • 高内聚,模块内的功能联系
  • 低耦合,模块间接口的复杂程度
  • 代码相对少,拓展性和可读性

测试的内容是模块

跑单元测试的时机:

  • 推代码到git repository之前,确保代码修改正确并不影响到其他模块
  • merge request之前,确保合并分支不会影响到target branch,配置CI

测试工具

Jest

  1. 测试代码组织
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));

test('', () => console.log('1 - test'));

describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));
  
  test('', () => console.log('2 - test'));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
  1. 测试内容 —— 变量和函数

jest里面是用expect,下面是一些常用的测试结果匹配检查

expect(2 + 2).toBe(4);

const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
expect(data).toHaveProperty('one',1);

const shoppingList = [  'diapers',  'kleenex',  'trash bags',  'paper towels',  'milk',];
expect(shoppingList).toContain('milk');

expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');

const f = jest.fn();
applyToAll(f);
expect(f).toHaveBeenCalledWith(beverage);

const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenCalledTimes(2);

\

  1. Mock

单元测试只测试代码,数据都需要使用假的测试数据。一般可以开一个 mocks

文件夹去管理相关的单元测试数据。

Mock就是将函数调用的输出变成你需要的测试数据

const drink = jest.fn();

drink.mockReturnValueOnce('lenmon')
drink.mockResolvedValueOnce('lenmon')

drink.mockRejectOnce()
drink.mockReturnValue('lenmon')
drink.mockResolvedValue('lenmon')

可以mock整个模块

import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});
  1. snapshop

jestjs.io/docs/snapsh…

reference :

noriste.github.io/reactjsday-…

jestjs.io/docs/using-…

jestjs.io/docs/expect

其他工具

sinon:sinonjs.org/#get-starte…

mocha:mochajs.org/


Nest.js实践

实际项目中会用到不同的框架,一般框架本身会对提供测试的工具和规范

这里讨论Nest.js

设计模式

OOP

面向对象编程OOP三大要素:封装、多态和继承

封装就是把抽象的数据和对数据进行的操作封装在一起,意味着OOP也具有模块化的特点

依赖注入

Dependencies are services or objects that a class needs to perform its function. Dependency injection, or DI, is a design pattern in which a class requests dependencies from external sources rather than creating them.

reference: angular.io/guide/depen…

Nestjs里面的模块

export interface ModuleMetadata {
    /**
     * Optional list of imported modules that export the providers which are
     * required in this module.
     */
    imports?: Array<Type<any> | DynamicModule | Promise<DynamicModule> | ForwardReference>;
    /**
     * Optional list of controllers defined in this module which have to be
     * instantiated.
     */
    controllers?: Type<any>[];
    /**
     * Optional list of providers that will be instantiated by the Nest injector
     * and that may be shared at least across this module.
     */
    providers?: Provider[];
    /**
     * Optional list of the subset of providers that are provided by this module
     * and should be available in other modules which import this module.
     */
    exports?: Array<DynamicModule | Promise<DynamicModule> | string | symbol | Provider | ForwardReference | Abstract<any> | Function>;
}

module的metadata能明确地反映出模块的依赖(imports)和服务边界(exports,controllers),providers则是能够在模块内注入的services

基于Nest.js的模块化设计,单元测试时就是要构建一个Test Module,模块的初始化就是要看测试内容的依赖关系而定

import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

describe('CatsController', () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
        controllers: [CatsController],
        providers: [CatsService],
      }).compile();

    catsService = moduleRef.get<CatsService>(CatsService);
    catsController = moduleRef.get<CatsController>(CatsController);
  });

  describe('findAll', () => {
    it('should return an array of cats', async () => {
      const result = ['test'];
      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

      expect(await catsController.findAll()).toBe(result);
    });
  });
});

测试结果评价

code coverage

jest --coverage

到底需要多少覆盖率?

需要依照具体项目而定,可以定70%~80%之间。

覆盖率过高,容易导致过多时间花在维护测试代码上

覆盖率过低,容易导致测试覆盖不到完整业务线而测不到一些流程控制的bug


如何提高覆盖率?

  1. 覆盖内容
  • 条件判断(if-else、switch)
  • 循环(for、while)
  • 异常捕获
  • 函数输入输出
  1. 代码优化

测试覆盖率会反作用于模块代码,减少冗余内容,有助于优化代码组织方式。


实际例子

talk is cheap show me the code


Frequency Ask

异步代码测试

jestjs.io/docs/asynch…

如何Mock一个第三方模块?

直接mock整个模块

import axios from 'axios';
jest.mock('axios');

另一种方法是寻找开源的包去支撑

Next.js中如何mock一个service?

const MockShopService = {
  getAmazonShopInfo: jest.fn().mockResolvedValue(fakeShop),
  getAmazonCookieInfo: jest.fn().mockResolvedValue(fakeCookie),
  requestPipe: jest.fn(),
};

const module = await Test.createTestingModule({
  providers: [
    ContactBuyerRequest,
    { provide: ShopService, useValue: MockShopService },
  ],
}).compile();