React-如何进行组件的单元测试

什么是单元测试

一般测试分成几个类型:单元测试、集成测试、功能测试。集成测试和功能测试不赘述。单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作,这里的单元是程序工作的最小工作单位。单元测试应该仅仅依赖输入,不依赖多余的环境,如果你的单元测试依赖很多环境,那么你可能需要的是集成测试

单元测试又可以根据开发模式分成以下两类:

  1. TDD, TDD指的是Test Drive Development,很明显的意思是测试驱动开发,也就是说我们可以从测试的角度来检验整个项目。大概的流程是先针对每个功能点抽象出接口代码,然后编写单元测试代码,接下来实现接口,运行单元测试代码,循环此过程,直到整个单元测试都通过。
  2. BDD指的是Behavior Drive Development,也就是行为驱动开发。行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。主要是从用户的需求出发,强调系统行为。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。

目前我接触到的项目都是BDD,国内的前端项目对单元测试重视程度没有那么高,TDD这种先编写单元测试的模式应用并不多。

但是但是,我真的想说,高覆盖率的单元测试,可以保证每次上线bug率大大降低,也是代码重构的基础。很多老项目,开发人员离职、新接手的人员不敢重构,慢慢称为团队负担、又不能下线,就是因为没有单元测试,改一点都怕出现不可测的bug。

单元测试的编写原则,建议参考 https://github.com/mawrkus/js-unit-testing-guide

React单元测试框架

这里分成两种情况:

  • 你的代码可以全部在node环境运行,不需要浏览器环境,那么选用Jest + Enzyme 。

Jest 是 Facebook 发布的一个开源的、基于 Jasmine 框架的 JavaScript 单元测试工具。提供了包括内置的测试环境 DOM API 支持、断言库、Mock 库等,还包含了 Spapshot Testing、 Instant Feedback 等特性。

Enzyme是Airbnb开源的 React 测试类库,提供了一套简洁强大的 API,并通过 jQuery 风格的方式进行DOM 处理,开发体验十分友好。不仅在开源社区有超高人气,同时也获得了React 官方的推荐。

  • 你的代码依赖浏览器环境,那么建议选用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);
  });
});

describe方法表示进行一组单元测试(Suites),it内部是一组测试中的某一个测试(Specs)。具体语法请参考 https://jasmine.github.io/tutorials/your_first_suite 和 https://jestjs.io/docs/en/api 。

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)$": "/__mocks__/file-mock.js",
    "\\.(css|scss)$": "/__mocks__/style-mock.js"
  },
  collectCoverage: true,
  collectCoverageFrom: [
    "**/*.{ts,tsx}",
    "!**/*.d.ts",
    "!**/node_modules/**"
  ],
  coverageDirectory: "/tests-coverage",
  coverageReporters: ["json", "html"]
}

其他可以配置的jest config,请参考 https://jestjs.io/docs/en/configuration 。

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);
  });
});

Enzyme还提供了其他接口,请参考 http://airbnb.io/enzyme/docs/api/ 。

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]

其他配置请参考 https://karma-runner.github.io/2.0/config/configuration-file.html 。

其他测试框架及总结

我还使用过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组件并触发生命周期。想要把单元测试跑起来,就需要运行器(runner),不依赖浏览器环境或者对浏览器的依赖可以通过mock数据跳过的项目,建议使用Jest,依赖浏览器环境且mock数据也无法跳过的项目,建议使用Karma。

你可能感兴趣的:(React,单元测试)