背景
"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
- 测试代码组织
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
- 测试内容 —— 变量和函数
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);
\
- 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));
});
- snapshop
reference :
noriste.github.io/reactjsday-…
其他工具
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
如何提高覆盖率?
- 覆盖内容
- 条件判断(if-else、switch)
- 循环(for、while)
- 异常捕获
- 函数输入输出
- 代码优化
测试覆盖率会反作用于模块代码,减少冗余内容,有助于优化代码组织方式。
实际例子
talk is cheap show me the code
Frequency Ask
异步代码测试
如何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();