1. 程式人生 > >React-如何進行元件的單元測試

React-如何進行元件的單元測試

什麼是單元測試

一般測試分成幾個型別:單元測試整合測試功能測試。整合測試和功能測試不贅述。單元測試是用來對一個模組、一個函式或者一個類來進行正確性檢驗的測試工作,這裡的單元是程式工作的最小工作單位。單元測試應該僅僅依賴輸入,不依賴多餘的環境,如果你的單元測試依賴很多環境,那麼你可能需要的是整合測試

單元測試又可以根據開發模式分成以下兩類:
1. TDD, TDD指的是Test Drive Development,很明顯的意思是測試驅動開發,也就是說我們可以從測試的角度來檢驗整個專案。大概的流程是先針對每個功能點抽象出介面程式碼,然後編寫單元測試程式碼,接下來實現介面,執行單元測試程式碼,迴圈此過程,直到整個單元測試都通過。
2. BDD指的是Behavior Drive Development,也就是行為驅動開發。行為驅動開發是一種敏捷軟體開發的技術,它鼓勵軟體專案中的開發者、QA和非技術人員或商業參與者之間的協作。主要是從使用者的需求出發,強調系統行為。BDD最初是由Dan North在2003年命名,它包括驗收測試和客戶測試驅動等的極限程式設計的實踐,作為對測試驅動開發的迴應。

目前我接觸到的專案都是BDD,國內的前端專案對單元測試重視程度沒有那麼高,TDD這種先編寫單元測試的模式應用並不多。

但是但是,我真的想說,高覆蓋率的單元測試,可以保證每次上線bug率大大降低,也是程式碼重構的基礎。很多老專案,開發人員離職、新接手的人員不敢重構,慢慢稱為團隊負擔、又不能下線,就是因為沒有單元測試,改一點都怕出現不可測的bug。

React單元測試框架

這裡分成兩種情況:

  • 你只使用了react,沒有其他需要編譯的程式碼,比如sass,那麼選用Jest + Enzyme 。

Jest 是 Facebook 釋出的一個開源的、基於 Jasmine 框架的 JavaScript 單元測試工具。提供了包括內建的測試環境 DOM API 支援、斷言庫、Mock 庫等,還包含了 Spapshot Testing、 Instant Feedback 等特性。

Enzyme是Airbnb開源的 React 測試類庫,提供了一套簡潔強大的 API,並通過 jQuery 風格的方式進行DOM 處理,開發體驗十分友好。不僅在開源社群有超高人氣,同時也獲得了React 官方的推薦。

  • 你有需要編譯的程式碼,需要使用webpack等打包工具,那麼建議選用Karma + Jasmine + Enzyme。

Karma 是一個用來搜尋測試檔案、編譯它們然後執行斷言的測試器,Angular團隊作品。

Jasmine 是一個斷言庫,它僅僅問“我們得到我們期待的東西了麼?”。它提供類似describe,expect 和 it的函式,也提供監聽一個函式或方法有沒有被觸發的監聽器。

Jest + Enzyme

Jest

Jest其實包括了斷言庫和執行器。斷言庫是寫單元測試時候使用的介面,Jest內建的斷言庫是Jasmine,使用語法如下:

describe("A suite is just a function", function() {
  var a;

  it("and so is a spec", function() {
    a = true;

    expect(a).toBe(true);
  });
});

Jest的runner使用很簡單,配置好config檔案就可以:

module.exports = {
  transform: {
    "^.+\\.tsx?$": "ts-jest"
  },
  testURL: "http://localhost",
  testRegex: "\\.(test|spec)\\.(jsx?|tsx?)$",
  testPathIgnorePatterns: [
    "/node_modules/"
  ],
  moduleFileExtensions: [
    "ts",
    "tsx",
    "js",
    "jsx"
  ],
  testResultsProcessor: "jest-junit",
  moduleNameMapper: {
    "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/file-mock.js",
    "\\.(css|scss)$": "<rootDir>/__mocks__/style-mock.js"
  },
  collectCoverage: true,
  collectCoverageFrom: [
    "**/*.{ts,tsx}",
    "!**/*.d.ts",
    "!**/node_modules/**"
  ],
  coverageDirectory: "<rootDir>/tests-coverage",
  coverageReporters: ["json", "html"]
}

Enzyme

Enzyme的基本用法:

import {mount, configure} from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16';

import Widget from './Widget ';

configure({ adapter: new Adapter() });

describe('try to use Enzyme', () => {
  let wrapper;
  let config = {};
  beforeAll(() => {
    wrapper = mount(<Widget />);
  });
  it('should have rendered widget', () => {
    expect(wrapper.hasClass('container')).toEqual(true);
  });
});

Karma + Jasmine + Enzyme

Jasmine和Enzyme上面都已經介紹,下面說說Karma。
Karma是一個執行單元測試的執行器(runner)。它可以執行webpack,這就提供了很大空間給我們。執行Karma也是通過一個配置檔案,這裡舉一個例子:

'use strict';
const path = require('path');
const fs = require('fs');
const argv = require('yargs').argv;

const KARMA_HOST_API_PATH = '/base/api';
const LOCAL_API_PATH = './api';

const api = argv.apiurl || process.env.JSAPI_URL || KARMA_HOST_API_PATH;

const isAPIBuilt = getAPIBuildStatus(api);
const files = getTestFiles(api);
const proxies = getProxies(api);
module.exports = (config) => {
  config.set({
    frameworks: [
      'jasmine',
    ],

    client: {
      // used in ./tests/test-main.js to load API from api
      args: [api, isAPIBuilt]
    },

    plugins: [
      'karma-jasmine',
      'karma-webpack',
      'karma-coverage',
      'karma-spec-reporter',
      'karma-chrome-launcher',
      'karma-phantomjs-launcher'
    ],

    files: files,

    preprocessors: {
      '**/*.+(ts|tsx)': ['webpack',
                  // 'coverage'
                ],
    },

    // optionally, configure the reporter
    coverageReporter: {
      type : 'html',
      dir : 'coverage/'
    },

    webpack: {
      // karma watches the test entry points
      // (you don't need to specify the entry option)
      // webpack watches dependencies
      devtool: 'source-map',
      mode: 'development',
      output: {
        libraryTarget: "amd"
      },
      resolve: {
        extensions: [".ts", ".tsx", ".js", ".jsx"],
        alias: {
          'builder': path.resolve(__dirname, 'builder/')
        },
      },
      module: {
        rules: [{
          test: /\.tsx?$/,
          exclude: /node_modules/,
          use: [
            {
              loader: 'ts-loader',
              options: {
                transpileOnly: true,
                configFile: require.resolve('./tsconfig.json')
              }
            }
          ]
        }, {
          test: /\.(scss|css)$/,
          use: [{
            loader: 'style-loader'
          }, {
            loader: 'css-loader',
            options: {
              sourceMap: process.env.NODE_ENV === 'production'? false: true
            }
          }, {
            loader: 'postcss-loader',
            options: {
              plugins: []
            }
          }, {
            loader: 'sass-loader',
            options: {
              sourceMap: process.env.NODE_ENV === 'production'? false: true
            }
          }]
        }, {
          test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
          use: {
            loader: 'url-loader',
            options: {
              limit: 10000,
              fallback: path.join(__dirname, './webpack/webpack-file-loader/main.js'),
              outputPath: (rPath, fullPath) => {
                return path.relative(__dirname, fullPath).replace(/\\/g, '/');
              },
              useRelativePath: true,
              name: fullPath => {
                return '../' + path.relative(__dirname, fullPath).replace(/\\/g, '/');
              }
            }
          }
        }]
      },
      stats: {
        colors: true,
        reasons: true
      },
      externals: [
        function(context, request, callback) {
          if (...) {
            return callback(null, "commonjs " + request);
          }
          callback();
        }
      ]
    },
    proxies: proxies,
    proxyValidateSSL: false,

    reporters: ['spec',
                // 'coverage'
              ],
    mime: {
      'text/x-typescript': ['ts','tsx']
    },

    colors: true,
    autoWatch: false,
    browsers: ['Chrome'], // Alternatively: 'PhantomJS', 'Chrome', 'ChromeHeadless'
    browserNoActivityTimeout: 100000,
    customLaunchers: {
      chrome_without_security: {
        base: 'Chrome',
        flags: ['--disable-web-security'],
        displayName: 'Chrome w/o security'
      }
    },
    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun: !!argv.singleRun
  });
};

function getAPIBuildStatus(api){
  if(isUsingKarmaHostApi(api)){
    return fs.existsSync(path.join(LOCAL_API_PATH, 'init.js'));
  }else{
    return !/\/src(\/)?$/.test(api);
  }
}
function getTestFiles(api){
  let tests = [
    './tests/test-main.js',
    {pattern: path.join(argv.root, '/**/*.apitest.+(js|ts|jsx|tsx)'), included: false}
  ]
  if(isUsingKarmaHostApi(api)){
    tests = tests.concat([
      {pattern: './api/**/*.+(js|html|xml|glsl|gif)', included: false, watched: false}
    ]);
  }

  return tests;
}
function getProxies(api){
  if(isUsingKarmaHostApi(api)){
    return {};
  }else{
    return {
      '/base/api/': {
        'target': api,
        'changeOrigin': false
      }
    }
  }
}

function isUsingKarmaHostApi(api){
  return api.indexOf(KARMA_HOST_API_PATH) > -1;
}

這個配置,允許使用者通命令列引數 --apiurl 配置一個當前單元測試依賴的api url,比如 lodash的cdn url。如果沒有提供這個引數,則會在根目錄下尋找./api/並host這個目錄下的所有檔案(通過{pattern: './api/**/*.+(js|html|xml|glsl|gif)', included: false, watched: false})。注意,因為提供url和不提供url,雖然請求相對路徑相同,但是url域不同,所以使用url時候需要啟用代理(proxies: proxies)。

在執行單元測試前,你可能需要載入一些配置,這裡通過./tests/test-main.js執行,它提前通過script標籤載入。在test-main.js中,可以通過window.__karma__.config.args獲得在node端注入的變數,注入變數通過args: [api, isAPIBuilt]

其他測試框架及總結

我還使用過intern(https://theintern.io/docs.html#Intern/4/docs/README.md),它也提供了一套單元測試框架。目前intern v4的doc還不全,配置coverage和report都有點費勁,比如配置junit report需要到它原始碼裡找都有哪些配置項,如何使用,不贅述。

總結起來也很簡單,就是寫React單元測試,語法和mock資料你需要熟悉Jasmine語法和Enzyme語法,Jasmine提供斷言庫,Enzyme可以虛擬的render元件並觸發生命週期,想要把單元測試跑起來,就需要執行器,簡單專案選用Jest,對webpack依賴比較重的專案可以使用Karma。