Jest 單元測(cè)試快速上手指南
原文鏈接: https://github.com/yinxin630/blog/issues/38
Jest[1] 是一款簡(jiǎn)單, 容易上手且功能十分強(qiáng)大的測(cè)試框架
安裝
yarn add -D jest
使用
創(chuàng)建 test 目錄, 添加 plus.spec.js 文件
describe('example',?()?=>?{
????it('should?equal?2',?()?=>?{
????????expect(1?+?1).toBe(2);
????});
});
執(zhí)行 yarn jest 或者 yarn jest test/plus.spec.js 運(yùn)行測(cè)試用例
成功結(jié)果

失敗結(jié)果

輸出測(cè)試覆蓋率
在根目錄創(chuàng)建 jest.config.js 配置文件
module.exports?=?{
????collectCoverage:?true,
};
創(chuàng)建 plus.js 模塊
module.exports?=?function?plus(a,?b)?{
????return?a?+?b;
}
修改測(cè)試用例使用模塊
const?plus?=?require('../plus');
describe('example',?()?=>?{
????it('should?equal?2',?()?=>?{
????????expect(plus(1,?1)).toBe(2);
????});
});
再次執(zhí)行測(cè)試, 輸出覆蓋率如下

在瀏覽器中打開(kāi) coverage/lcov-report/index.html 可以瀏覽覆蓋率結(jié)果頁(yè)面


忽略部分文件或者代碼行的覆蓋率
修改 plus.ts 模塊, 添加更多分支
export?default?function?plus(a:?number,?b:?number)?{
????if?(a?+?b?>?100)?{
????????return?0;
????}?else?if?(a?+?b?0)?{
????????return?0;
????}?else?{
????????return?a?+?b;
????}
}
重新執(zhí)行測(cè)試, 覆蓋率輸出結(jié)果

你可以完善測(cè)試用例, 或者可能有些文件(譬如 config)和代碼分支并不需要測(cè)試, 可以將其在測(cè)試覆蓋率結(jié)果中排除, 參考如下配置
忽略目錄下所有文件
在 jest.config.js 中添加
collectCoverageFrom:?[
????'**/*.{ts,tsx}',
????'!**/node_modules/**',
????'!**/[directory?path]/**',
],
以 ! 開(kāi)頭的表示忽略與其匹配的文件
忽略單個(gè)文件
在該文件頂部添加 /* istanbul ignore file */
忽略一個(gè)函數(shù), 一塊分支邏輯或者一行代碼
在該函數(shù), 分支邏輯或者代碼行的上一行添加 /* istanbul ignore next */
支持 Typescript
執(zhí)行 yarn add -D typescript ts-jest @types/jest 安裝 typescript 和聲明
并在 jest.config.js 中添加 preset: 'ts-jest'
將 plus.js 重命名為 plus.ts
export?default?function?plus(a:?number,?b:?number)?{
????return?a?+?b;
}
同樣的, 將 plus.spec.js 重命名為 plus.spec.ts
import?plus?from?'../plus'
describe('example',?()?=>?{
????it('should?equal?2',?()?=>?{
????????expect(plus(1,?1)).toBe(2);
????});
});
執(zhí)行測(cè)試, 結(jié)果和之前一致
執(zhí)行單測(cè)時(shí)不校驗(yàn) ts 類(lèi)型
有時(shí)你可能會(huì)希望不校驗(yàn) ts 類(lèi)型, 僅執(zhí)行代碼測(cè)試, 比如需要在 CI 中將類(lèi)型校驗(yàn)和單元測(cè)試分為兩個(gè)任務(wù)
在 jest.config.js 中添加如下內(nèi)容
globals:?{
????'ts-jest':?{
????????isolatedModules:?true,
????},
}
測(cè)試 React 組件
安裝 react 依賴(lài) yarn add react react-dom 和聲明 yarn add -D @types/react安裝 react 測(cè)試庫(kù) yarn add -D @testing-library/react @testing-library/jest-dom
添加 typescript 配置文件 tsconfig.json
{
????"compilerOptions":?{
????????"target":?"es2018",
????????"strict":?true,
????????"moduleResolution":?"node",
????????"jsx":?"react",
????????"allowSyntheticDefaultImports":?true,
????????"esModuleInterop":?true,
????????"lib":?["es2015",?"es2016",?"es2017",?"dom"]
????},
????"exclude":?["node_modules"]
}
新增測(cè)試組件 Title.tsx
import React from 'react';
function Title() {
return (
Title
);
}
export default Title;
新增測(cè)試用例 test/Title.spec.tsx
/**
* @jest-environment jsdom
*/
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Title from '../Title';
describe('Title', () => {
it('should render without error', () => {
const { getByText } = render( );
const $title = getByText('Title');
expect($title).toBeInTheDocument();
});
});
執(zhí)行 yarn jest test/Title.spec.ts 查看結(jié)果
處理靜態(tài)資源引用
react 組件有時(shí)引用一些靜態(tài)資源, 譬如圖片或者 css 樣式表, webpack 會(huì)正確的處理這些資源, 但是對(duì) Jest 來(lái)講, 這些資源是無(wú)法識(shí)別的
創(chuàng)建 Title.less 樣式表
h1 {
color: red;
}
修改 Ttitle.tsx, 添加樣式引用 import './Title.less';
執(zhí)行測(cè)試會(huì)報(bào)錯(cuò)

我們需要配置 transform 對(duì)其處理
在根目錄創(chuàng)建 jest.transformer.js
const?path?=?require('path');
module.exports?=?{
????process(src,?filename)?{
????????return?`module.exports?=?${JSON.stringify(path.basename(filename))};`;
????},
};
這里是將資源文件名作為模塊導(dǎo)出內(nèi)容
修改 jest.config.js 添加如下配置
transform:?{
????'\\.(less)$':?'/jest.transformer.js' ,?//?正則匹配,?處理?less?樣式
},
然后重新執(zhí)行測(cè)試就可以了
處理 css in js
如果你使用了類(lèi)似 linaria[2] 這種 css in js 方案, 其中的 css 樣式模板字符串是不支持運(yùn)行時(shí)編譯的
修改 Title.tsx
import React from 'react';
import { css } from 'linaria';
const title = css`
color: red;
`;
function Title() {
return Title
;
}
export default Title;
運(yùn)行測(cè)試會(huì)報(bào)錯(cuò)

linaria 是通過(guò) babel 插件將其預(yù)編譯為 class 名的, 這里可以 mock 一下 css 函數(shù), 返回一個(gè)隨機(jī)值作為 class 名
在根目錄創(chuàng)建 jest.setup.js
jest.mock('linaria',?()?=>?({
????css:?jest.fn(()?=>?Math.floor(Math.random()?*?(10?**?9)).toString(36)),
}));
修改 jest.config.js 添加如下配置
setupFilesAfterEnv:?['./jest.setup.js'],
重新執(zhí)行測(cè)試就可以了
測(cè)試交互事件
新增 Count.tsx 組件
import React, { useState } from 'react';
function Count() {
const [count, updateCount] = useState(0);
return (
{count}
);
}
export default Count;
新增 test/Count.spec.tsx 組件
/**
* @jest-environment jsdom
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Count from '../Count';
describe('Count', () => {
it('should render without error', () => {
const { getByTestId } = render( );
const $count = getByTestId('count');
const $button = getByTestId('button');
expect($count).toHaveTextContent('0');
fireEvent.click($button);
expect($count).toHaveTextContent('1');
});
});
這里通過(guò) testId 來(lái)查找元素, 使用 fireEvent[3] 觸發(fā) click 事件
測(cè)試函數(shù)調(diào)用
新增 Button.tsx 組件
import React from 'react';
type Props = {
onClick: () => void;
};
function Button({ onClick }: Props) {
return ;
}
export default Button;
添加 test/Button.spec.tsx 測(cè)試用例
/**
* @jest-environment jsdom
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Button from '../Button';
describe('Button', () => {
it('should render without error', () => {
const handleClick = jest.fn(); // mock 函數(shù)
const { getByText } = render(); // 傳遞 props
const $button = getByText('button');
fireEvent.click($button);
expect(handleClick).toHaveBeenCalled(); // 期望其被調(diào)用
});
});
測(cè)試包含定時(shí)器的邏輯
//?timer.ts
let?cache?=?'cache';
export?default?function?timer()?{
????setTimeout(()?=>?{
????????cache?=?'';
????},?1000);
????return?cache;
}
//?test/timer.spec.ts
import?timer?from?'../timer'
jest.useFakeTimers();?//?替代原生計(jì)時(shí)器
describe('timer',?()?=>?{
????it('should?clear?cache?after?timer?out',?()?=>?{
????????expect(timer()).toBe('cache');
????????jest.advanceTimersByTime(1000);?//?讓計(jì)時(shí)器前進(jìn)?1000ms
????????expect(timer()).toBe('');
????})
})
mock 依賴(lài)模塊
要測(cè)試的模塊可能依賴(lài)于其他模塊或者第三方 npm 包的結(jié)果, 我們可以使用 Mock Functions[4] 對(duì)其進(jìn)行 mock
//?test/mock.spec.ts
import?{?mocked?}?from?'ts-jest/utils';
import?plus?from?'../plus';
jest.mock('../plus');
describe('mock',?()?=>?{
????it('should?return?mock?value',?()?=>?{
????????mocked(plus).???(50);
????????expect(plus(1,?1)).toBe(50);
????});
});
還有官網(wǎng) mock axios npm 模塊的例子 https://jestjs.io/docs/en/mock-functions#mocking-modules
mock 環(huán)境變量和命令行參數(shù)
有的模塊會(huì)從環(huán)境變量和命令行參數(shù)取值, 并且可能是在模塊初始化時(shí)獲取的
//?process.ts
const?{?env,?argv?}?=?process;
export?function?getEnvironmentValue()?{
????return?env.Value;
}
export?function?getProcessArgsValues()?{
????return?argv[2];
}
這種情況我們需要在每個(gè)測(cè)試用例中, 使用動(dòng)態(tài) require 來(lái)運(yùn)行時(shí)引入改模塊, 并且設(shè)置其每次引入時(shí)刪除 cache
//?test/process.spec.ts
describe('mock?process',?()?=>?{
????beforeEach(()?=>?{
????????jest.resetModules();
????});
????it('should?return?environment?value',?()?=>?{
????????process.env?=?{
????????????Value:?'value',
????????};
????????const?{?getEnvironmentValue?}?=?require('../process');
????????expect(getEnvironmentValue()).toBe('value');
????});
????it('should?return?process?args?value',?()?=>?{
????????process.argv?=?['value'];
????????const?{?getProcessArgsValues?}?=?require('../process');
????????expect(getProcessArgsValues()).toBe('value');
????});
});
參考資料
Jest: https://jestjs.io/
[2]linaria: https://github.com/yinxin630/blog/issues/36
[3]fireEvent: https://testing-library.com/docs/dom-testing-library/api-events
[4]Mock Functions: https://jestjs.io/docs/en/mock-function-api
推薦閱讀
1、力扣刷題插件
2、你不知道的 TypeScript 泛型(萬(wàn)字長(zhǎng)文,建議收藏)
4、immutablejs 是如何優(yōu)化我們的代碼的?
7、【校招面經(jīng)分享】好未來(lái)-北京-視頻面試
?關(guān)注加加,星標(biāo)加加~
?
如果覺(jué)得文章不錯(cuò),幫忙點(diǎn)個(gè)在看唄
