Jest 單元測試快速上手指南

技術交流羣: https://fiora.suisuijiang.com/
原文連接: https://github.com/yinxin630/blog/issues/38css

Jest[1] 是一款簡單, 容易上手且功能十分強大的測試框架
html

安裝

yarn add -D jestnode

使用

建立 test 目錄, 添加 plus.spec.js 文件react

describe('example', () => {
    it('should equal 2', () => {
        expect(1 + 1).toBe(2);
    });
});

執行 yarn jest 或者 yarn jest test/plus.spec.js 運行測試用例webpack

成功結果ios

失敗結果git

輸出測試覆蓋率

在根目錄建立 jest.config.js 配置文件github

module.exports = {
    collectCoveragetrue,
};

建立 plus.js 模塊web

module.exports = function plus(a, b{
    return a + b;
}

修改測試用例使用模塊typescript

const plus = require('../plus');

describe('example', () => {
    it('should equal 2', () => {
        expect(plus(1, 1)).toBe(2);
    });
});

再次執行測試, 輸出覆蓋率以下


在瀏覽器中打開 coverage/lcov-report/index.html 能夠瀏覽覆蓋率結果頁面


忽略部分文件或者代碼行的覆蓋率

修改 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;
    }
}

從新執行測試, 覆蓋率輸出結果


你能夠完善測試用例, 或者可能有些文件(譬如 config)和代碼分支並不須要測試, 能夠將其在測試覆蓋率結果中排除, 參考以下配置

  1. 忽略目錄下全部文件

jest.config.js 中添加

collectCoverageFrom: [
    '**/*.{ts,tsx}',
    '!**/node_modules/**',
    '!**/[directory path]/**',
],

! 開頭的表示忽略與其匹配的文件

  1. 忽略單個文件

在該文件頂部添加 /* istanbul ignore file */

  1. 忽略一個函數, 一塊分支邏輯或者一行代碼

在該函數, 分支邏輯或者代碼行的上一行添加 /* istanbul ignore next */

支持 Typescript

執行 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(11)).toBe(2);
    });
});

執行測試, 結果和以前一致

執行單測時不校驗 ts 類型

有時你可能會但願不校驗 ts 類型, 僅執行代碼測試, 好比須要在 CI 中將類型校驗和單元測試分爲兩個任務 在 jest.config.js 中添加以下內容

globals: {
    'ts-jest': {
        isolatedModulestrue,
    },
}

測試 React 組件

安裝 react 依賴 yarn add react react-dom 和聲明 yarn add -D @types/react安裝 react 測試庫 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"]
}

新增測試組件 Title.tsx

import React from 'react';

function Title() {
return (
<h1>Title</h1>
);
}

export default Title;

新增測試用例 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(<Title />);
const $title = getByText('Title');
expect($title).toBeInTheDocument();
});
});

執行 yarn jest test/Title.spec.ts 查看結果

處理靜態資源引用

react 組件有時引用一些靜態資源, 譬如圖片或者 css 樣式表, webpack 會正確的處理這些資源, 可是對 Jest 來說, 這些資源是沒法識別的

建立 Title.less 樣式表

h1 {
color: red;
}

修改 Ttitle.tsx, 添加樣式引用 import './Title.less';

執行測試會報錯


咱們須要配置 transform 對其處理

在根目錄建立 jest.transformer.js

const path = require('path');

module.exports = {
    process(src, filename) {
        return `module.exports = ${JSON.stringify(path.basename(filename))};`;
    },
};

這裏是將資源文件名做爲模塊導出內容

修改 jest.config.js 添加以下配置

transform: {
    '\\.(less)$''<rootDir>/jest.transformer.js'// 正則匹配, 處理 less 樣式
},

而後從新執行測試就能夠了

處理 css in js

若是你使用了相似 linaria[2] 這種 css in js 方案, 其中的 css 樣式模板字符串是不支持運行時編譯的

修改 Title.tsx

import React from 'react';
import { css } from 'linaria';

const title = css`
color: red;
`;

function Title() {
return <h1 className={title}>Title</h1>;
}

export default Title;

運行測試會報錯

linaria 是經過 babel 插件將其預編譯爲 class 名的, 這裏能夠 mock 一下 css 函數, 返回一個隨機值做爲 class 名

在根目錄建立 jest.setup.js

jest.mock('linaria', () => ({
    css: jest.fn(() => Math.floor(Math.random() * (10 ** 9)).toString(36)),
}));

修改 jest.config.js 添加以下配置

setupFilesAfterEnv: ['./jest.setup.js'],

從新執行測試就能夠了

測試交互事件

新增 Count.tsx 組件

import React, { useState } from 'react';

function Count() {
const [count, updateCount] = useState(0);
return (
<div>
<span data-testid="count">{count}</span>
<button data-testid="button" onClick={() => updateCount(count + 1)}>
+1
</button>
</div>
);
}

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(<Count />);
const $count = getByTestId('count');
const $button = getByTestId('button');
expect($count).toHaveTextContent('0');
fireEvent.click($button);
expect($count).toHaveTextContent('1');
});
});

這裏經過 testId 來查找元素, 使用 fireEvent[3] 觸發 click 事件

測試函數調用

新增 Button.tsx 組件

import React from 'react';

type Props = {
onClick: () => void;
};

function Button({ onClick }: Props) {
return <button onClick={onClick}>button</button>;
}

export default Button;

添加 test/Button.spec.tsx 測試用例

/**
* @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 函數
const { getByText } = render(<Button onClick={handleClick} />); // 傳遞 props
const $button = getByText('button');
fireEvent.click($button);
expect(handleClick).toHaveBeenCalled(); // 指望其被調用
});
});

測試包含定時器的邏輯

// timer.ts
let cache = 'cache';

export default function timer({
    setTimeout(() => {
        cache = '';
    }, 1000);
    return cache;
}
// test/timer.spec.ts
import timer from '../timer'

jest.useFakeTimers(); // 替代原生計時器

describe('timer'() => {
    it('should clear cache after timer out'() => {
        expect(timer()).toBe('cache');
        jest.advanceTimersByTime(1000); // 讓計時器前進 1000ms
        expect(timer()).toBe('');
    })
})

mock 依賴模塊

要測試的模塊可能依賴於其餘模塊或者第三方 npm 包的結果, 咱們能夠使用 Mock Functions[4] 對其進行 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(11)).toBe(50);
    });
});

還有官網 mock axios npm 模塊的例子 https://jestjs.io/docs/en/mock-functions#mocking-modules

mock 環境變量和命令行參數

有的模塊會從環境變量和命令行參數取值, 而且多是在模塊初始化時獲取的

// process.ts
const { env, argv } = process;

export function getEnvironmentValue({
    return env.Value;
}

export function getProcessArgsValues({
    return argv[2];
}

這種狀況咱們須要在每一個測試用例中, 使用動態 require 來運行時引入改模塊, 而且設置其每次引入時刪除 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');
    });
});

參考資料

[1]

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


本文分享自微信公衆號 - 牧碼的星星(gh_0d71d9e8b1c3)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索