柒青衿

如何进行React组件单元测试

柒青衿 · 2016-12-21翻译 · 578阅读 原文链接

单元测试是定义我们构建的组件必须实现哪些功能的很好方式。它允许我们尽可能粒子化地测试我们组件的结构。我发现,单元测试使我在使用React时,写更多功能性的代码。我们将使用Karma,Jasmine和Enzyme开始React单元测试。

如果只对示例代码感兴趣,请戳 https://github.com/aktof/example-react-unit-testing.


什么是Karma,Jasmine 和 Enzyme?

为我们测试运行和写断言将用到这三个库。它们定义了我们测试环境的不同方面。

Karma 是一个用来搜索测试文件、编译它们然后运行断言的测试器。

Jasmine 是一个断言库,它仅仅问“我们得到我们期待的东西了么?”。它提供类似describeexpectit的函数,也提供监听一个函数或方法有没有被触发的监听器。

Enzyme 是一个React测试工具库。Enzyme提供渲染和遍历React组件的方法,可以用来测试与React的render、mount和事件有关的断言。

请记住,这不是进行单元测试的唯一方法。有许多不同的库和配置可以完成同一件事。


示例应用程序

首先我们需要新建一个展示组件(dumb component) Button。规则是它接受和渲染 label prop,然后当它被单击时调用 onClick prop。

目录结构:

lib/
src/
-- components/
----button/
------index.js
------spec.js
-- index.js
karma.conf.js
package.json
webpack.config.js

入口文件,即webpack开始打包的文件:

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Button from 'components/button';
const App = () => (
  <Button
    label='Replicator'
    onClick={() => console.log('Tea, Earl Grey, Hot.')} />
);
ReactDOM.render(
  <App />,
  document.getElementByID('root'),
);

入口文件中引入的Button

// src/components/button/index.js
import React from 'react';
export default () => null;

为了构建我们的代码,我们需要配置webpack入口文件、出口文件以及如何编译代码。下面是解析React的最基本配置。

最基本的webpack配置:

// webpack.config.js
var path = require('path');
module.exports = {
  entry: [
    path.resolve(__dirname, 'src'),
  ],
  output: {
    filename: 'app.bundle.js',
    path: path.resolve(__dirname, 'lib'),
  },
  module: {
    loaders: [
      {
        exclude: /node_modules/,
        loader: 'babel',
        test: /\.js$/,
      },
      {
        loader: 'json',
        test: /\.json$/,
      },
    ],
  },
  resolve: {
    root: path.resolve(__dirname, 'src'),
    extensions: [
      '',
      '.js',
      '.json',
    ],
    moduleDirectories: [
      'src',
      'node_modules',
    ],
  },
};

一旦完成以上配置,我们可以在package.json中添加一个构件脚本来构件客户端包。

// package.json
{
  ...
  "scripts": {
    ...
    "build": "webpack",
  },
  ...
}

在终端中输入npm run start来运行它。


配置测试运行器

Karma在默认情况下会寻找一个叫karma.conf.js的文件来定义测试环境。在这个文件中,我们可以配置测试文件、使用的浏览器、插件。

我们主要会用到 karma-jasminekarma-webpack 两个插件。

karma-jasmine 提供带有Jasmine功能的环境。包括describeitexpect 等。

karma-webpack允许运行测试器编译所有JavaScript文件 — 测试代码和源代码都包括。为了达到这个目的,我们仅仅需要引入之前写好的配置文件然后更新它。

// karma.conf.js
var webpackConfig = require('./webpack.config.js');
var config = function(config) {
  return config.set({
    basePath: '',
    browsers: [
      'PhantomJS',
    ],
    frameworks: [
      'jasmine',
    ],
    files: [
      'src/**/spec.js',
    ],
    preprocessors: {
      'src/**/*.js': [
        'webpack',
        'sourcemap',
      ],
      'src/**/spec.js': [
        'webpack',
        'sourcemap',
      ],
    },
 /**
     *  1) We need to define that these variables are available globally.
     */
 webpack: Object.assign(
      {},
      webpackConfig,
      {
        externals: {
          'react/addons': true,
          'react/lib/ExecutionEnvironment': true,
          'react/lib/ReactContext': true,
        },
      }
    ),
 /**
     *  2) Even if you're using express, the webpackServer option is required to compile code through karma-webpack.
     */
 webpackServer: {
      noInfo: true,
    },
    reporters: [
      'progress',
    ],
    colors: true,
    autoWatch: true,
    plugins: [
      'karma-webpack',
      'karma-jasmine',
      'karma-sourcemap-loader',
      'karma-phantomjs-launcher',
    ],
  });
};
module.exports = config;

太棒了,现在我们有一个功能完备的运行测试器啦。我们可以喝杯酒看着机器运行,多么享受的时光。


它是做什么的?

在我们投入建立断言环境前, 让我们先弄明白单元测试是干什么的。简而言之,目标是测试一个应用的最小片段。

当写单元测试时,在头部声明“为了让这段代码像我想的那样运行至少需要什么”。这会帮助你决定写什么样的断言。当你想维护你的基本代码时,你可能会期待这些测试告诉你最近的更改或者模块更新是否改变了组件的原功能。

写一些断言

现在到了最沉闷也最有趣的部分了,写button的技术参数说明。我们想测试的三件事是:

  • 组件 Button 可以渲染一个 button 标签。

  • 组件 Button 可以渲染传给它的label文本。

  • 如果onClick prop存在,组件 Button 可以调用它。

import React from 'react';
import {shallow} from 'enzyme';
import Button from 'components/button';
describe('Button', () => {
  let wrapper;
  const props = {
    onClick: jasmine.createSpy('onClick'),
    label: 'Fire Photon Torpedos',
  };
beforeEach(() => {
    wrapper = shallow(<Button {...props} />);
  });
it('should contain a button element', () => {
    expect(wrapper.is('button')).toBe(true);
  });
it('should contain the label passed to it', () => {
    expect(wrapper.text()).toBe(props.label);
  });
it('should call the onClick handler when the button is clicked', () => {
    wrapper.simulate('click');
expect(props.onClick).toHaveBeenCalled();
  });
});

像“应该包含.someClassName”这样的DOM断言怎么办?我个人更喜欢不检查如此小的点,除非它是一种不同情况下渲染的产品。例如,在一个已经认证的组件中渲染<LoggedIn />或者<LoggedOut />取决于当前的认证状态。


运行你的测试

我们将向package.json中增加两个script,这样我们就可以很容易的运行单个测试或者一个时刻随源码改变的测试(类似webpack热加载)。

// package.json
{
  ...
  "scripts": {
    ...
    "test": "karma start --single-run",
    "test:watch": "karma start",
  },
  ...
}

增加了这些后,运行npm run test:watch

☠️ Failure ☠️


确保一切正常

现在我们有一些有待实现的目标,现在让我们完成它们。

// src/components/button/index.js
import React from 'react';
export default ({onClick, label}) => (
  <button onClick={onClick}>
    {label}
  </button>
);

这是一个明显的展示组件,它能完成我们的目标!


结论

希望这篇文章能让你明白单元测试的重点是什么,我们能用什么工具写和运行单元测试,如何自动化做这些。

完整的实例代码请戳 https://github.com/aktof/example-react-unit-testing.

相关文章