React單元測試實戰

蘇格團隊發表於2019-04-25
  • 蘇格團隊
  • 作者:Dee

前言

單元測試的好處

  • 可保證得到結果的一致性,提高專案、元件穩定性。
  • 開發者按單元測試思路去寫程式碼,可清晰程式碼結構,提高程式碼的可讀性。

由於筆者開發的專案越來越大,公共元件的複用性高,故其穩定性尤為重要。因此,引入單元測試刻不容緩。

單元測試的不好

  • 會佔用一定的開發成本,增加開發工作量。
  • 舊專案加入單元測試改動很大,會有一定的風險。
  • 會有一定的學習成本,對開發者要求比較高。
  • 如果在一些複用性很低的元件使用單元測試,成效不大且開發成本高。

選型

在做專案單元測試前,筆者參考了網上的一些文章以及官方文件,最後選型為Jest + react-test-renderer + Enzyme。

  • Jest

    Jest 是 Facebook 出品的一個測試框架,相對其他測試框架,其一大特點就是就是整合了 Mocha,chai,jsdom,sinon等功能,內建了常用的測試工具,比如自帶斷言、測試覆蓋率工具,實現了開箱即用。

  • react-test-render

    配合react-test-render,Jest 可提供了快照測試功能。

    首次執行快照測試,會產生一個可讀的快照,再次測試時會通過比對快照檔案和新產生的快照判斷測試是否通過。

    Jest在執行的時候如果發現toMatchSnapshot方法,會在同級目錄下生成一個__ snapshots__資料夾用來存放快照檔案,以後每次測試的時候都會和第一次生成的快照進行比較。

  • Enzyme

    React官方已經提供了一個測試工具庫:react-dom/test-utils。但是用起來不夠方便,於是有了一些第三方的封裝庫,比如Airbnb公司的Enzyme。其兩大特點:

    • 提供了一套簡潔強大的 API,並內建Cheerio
    • 實現了jQuery風格的方式進行DOM 處理,開發體驗十分友好

    三種渲染方法

    shallow:淺渲染,是對官方的Shallow Renderer的封裝。將元件渲染成虛擬DOM物件,只會渲染第一層,子元件將不會被渲染出來,使得效率非常高。不需要DOM環境, 並可以使用jQuery的方式訪問元件的資訊

    render:靜態渲染,它將React元件渲染成靜態的HTML字串,然後使用Cheerio這個庫解析這段字串,並返回一個Cheerio的例項物件,可以用來分析元件的html結構

    mount:完全渲染,它將元件渲染載入成一個真實的DOM節點,用來測試DOM API的互動和元件的生命週期。用到了jsdom來模擬瀏覽器環境

    三種方法中,shallowmount因為返回的是DOM物件,可以用simulate進行互動模擬,而render方法不可以。一般shallow方法就可以滿足需求,如果需要對子元件進行判斷,需要使用render,如果需要測試元件的生命週期,需要使用mount方法。

    注意:enzyme還需要根據React的版本安裝介面卡,介面卡對應表如下:

React單元測試實戰

方案

前面說了這麼多,是時候上程式碼了。

  • 目錄

    筆者在根目錄新建一個unitTest目錄,其目錄結構為:

    React單元測試實戰

    • jest.config.js:jest配置檔案

    • mocks:mock檔案目錄

    • components:專案的公共元件單元測試用例目錄

    • components/__ snapshots __:執行單元測試時自動生成的快照存放目錄

  • 安裝(由於筆者是react16版本,所以安裝的介面卡版本為enzyme-adapter-react-16)

    npm install jest enzyme enzyme-adapter-react-16 react-test-renderer

  • 配置

    Jest支援直接在package.json檔案寫入配置,但筆者有輕微潔癖,喜歡把配置檔案寫到unitTest裡面,方便查詢以及閱讀。

    // package.json
    {
        "scripts": {
            "jest": "jest --config ./unitTest/jest.config.js", // 單元測試
            "jestupdate": "jest --config ./unitTest/jest.config.js --updateSnapshot" // 單元測試快照更新
            "jestreport": "jest --config ./unitTest/jest.config.js --coverage" // 單元測試並生成覆蓋率報告
        }
    }
    複製程式碼
    // jest.config.js
    module.exports = {
        testURL: 'http://localhost/',
        setupFiles: [],
        moduleFileExtensions: ['js', 'jsx'],
        testPathIgnorePatterns: ['/node_modules/'],
        testRegex: '.*\\.test\\.js$',
        collectCoverage: false,
        collectCoverageFrom: ['src/components/**/*.{js}'],
        moduleNameMapper: {
            '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
                '<rootDir>/mocks/fileMock.js',
            '\\.(css|less|scss)$': '<rootDir>/mocks/styleMock.js'
        }
    };
    
    複製程式碼
    • testURL: jsdom執行url,預設為"about:blank",如果不設定,會在嘗試訪問localStorage出錯。
    • setupFiles:執行測試程式碼前,Jest會先執行setupFile指定的配置檔案來初始化測試環境。
    • moduleFileExtensions:支援單元測試的副檔名。
    • testPathIgnorePatterns:匹配忽略檔案規則。
    • testRegex:匹配測試檔案規則。
    • collectCoverage:是否生成測試覆蓋報告,開啟會增加測試時間。
    • collectCoverageFrom:指示應收集覆蓋率資訊的一組檔案。如果檔案與指定的glob模式匹配,即使此檔案不存在測試,也將為其收集覆蓋率資訊,並且測試套件中從不需要它。
    • moduleNameMapper:可用於將模組路徑對映到不同的模組。預設情況下,預設將所有影像對映到影像存根模組,但如果找不到模組,可配置此選項。
  • mock檔案

    // fileMock.js
    module.exports = {};
    複製程式碼
    // styleMock.js
    module.exports = {};
    複製程式碼
  • 編寫單元測試

    // button.test.js
    import Button from '../../src/common/components/Button';
    import renderer from 'react-test-renderer';
    import React from 'react';
    import { shallow, configure } from 'enzyme'; // shallow(淺渲染,只渲染父元件)
    import Adapter from 'enzyme-adapter-react-16'; // 適應React-16
    configure({ adapter: new Adapter() }); // 適應React-16,初始化
    const props = {
        text: '按鈕測試用例',
        type: 'white',
        style: { marginTop: 15 },
        size: 'big',
        disabled: false,
        height: 'middle',
        isLock: true,
        cname: 'hello',
        onClick: () => {}
    };
    describe('test Button', () => {
        it('button render correctly', () => {
            const tree = renderer.create(<Button {...props} />).toJSON();// 生成快照
            expect(tree).toMatchSnapshot(); // 匹配之前的快照
        });
    
        it('button has class', () => {
            const item = shallow(<Button {...props} />); //淺渲染
            expect(item.hasClass('hello')).toBe(true); // 斷言有item有hello的className
        });
    });
    
    
    複製程式碼

後記

注意事項:

1、如果不配置testURL,會報錯:localStorage is not available for opaque origins

2、本文件只講述筆者的實踐方案以供參考,關於Jest、enzyme的具體介紹、用法可參考

相關文章