React测试的那些事(二) 方法对比&原则

700 阅读7分钟
写在前面的话
  • ~react-testing-library~ 个人喜欢 react-testing-library,但是一般大家使用Enzyme。我会先写一个enzyme的例子,后面的例子使用react-testing-library来写。
  • ~示例大纲~ 示例遵循的模板:首先展示React组件,再附上测试和解说。也可以看开头repo的链接。
  • ~配置项~ 默认使用create-react-app作为脚手架搭建了示例基本结构,省略了手动配置的步骤。
  • ~Sinon, mocha, chai~ Jest中已经默认提供了很多sinon的方法,所以无需引入sinon。Mocha 和 chai 与jest功能类似,可以替换。Jest的预先配置功能可以与咱们的项目良好配合,所以无需Mocha和chai。
  • ~组件命名的格式~ 本文命名React组件的格式是<TestSomething />
  • ~npm test and jest watch mode~ yarn test可用的, npm test在jest watch mode中会失效。
  • ~testing a single file~yarn test + filename
  • ~React Hooks vs Classes~ 本文中多用React hooks,由于react-testing-library的强大功能,class 可以和 React Hooks一起用。

有了以上这些信息,我们来看代码吧!


Enzyme

~安装~

npm install enzyme enzyme-to-json  enzyme-adapter-react-16

~引入依赖项~

import React from 'react';
import ReactDOM from 'react-dom';
import Basic from '../basic_test';

import Enzyme, { shallow, render, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() })
  • 我们先引入最基本的项(前三个),React和组件。
  • 接着我们引入 Enzyme、从enzyme-to-json库中引入toJson函数。toJson函数能够把shallow 渲染的组件转成json然后保存到snapshot file中 。
  • 最后我们引入Adapter,它帮助enzymereact v16 协作起来,然后进行初始化。

~react-test-renderer~

react自带的测试库,能够替代enzymereact-test-renderer的语法如下👇

import TestRenderer from 'react-test-renderer';
import ShallowRenderer from 'react-test-renderer/shallow';


Basic Test with React-test-renderer
it('renders correctly react-test-renderer', () => {
  const renderer = new ShallowRenderer();
  renderer.render(<Basic />);
  const result = renderer.getRenderOutput();

  expect(result).toMatchSnapshot();
});

react-test-renderer也认为enzyme在语法上更好一些,ps:它俩做一样的事情。


SnapShot Testing

来看一个 snapshot 测试,文件名为basic_test.test.jsx。

// basic_test.test.jsx
it('renders correctly enzyme', () => {
  const wrapper = shallow(<Basic />)

  expect(toJson(wrapper)).toMatchSnapshot();
});

在运行这句命令之前,__snapshots__ 文件夹和 test.js.snap 文件已经被自动创建了。每次新的snapshot会与之前的snapshot进行对比,如果snapshot没有变化测试通过,否则不通过。

所以本质上说,snapshot测试通过一行一行地对比组件的变化,这些变化被称为diff

👇来看一个基本的react组件,文件名为basic_test.jsx。

// basic_test.jsx
import React from 'react';

const Basic = () => {
  return (
    <div >
      <h1> Basic Test</h1>
      <p> This is a basic Test Component</p>
    </div>
  );
}

export default Basic;

basic_test.jsx 进行basic_test.test.jsx 的snapshot测试,会生成下面的React DOM 节点🌲

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders correctly enzyme 1`] = `
<div>
  <h1>
     Basic Test
  </h1>
  <p>
     This is a basic Test Component
  </p>
</div>
`;

并且控制台输出

如果修改basic_test.jsx组件如下👇

import React from 'react';

const Basic = () => {
  return (
    <div >
      <h1> Basic Test</h1>

    </div>
  );
}

export default Basic;

snapshot会失败

还会显示Diff内容, -代表这一行被删掉了

这时,w开启activate watch mode 然后 点击u更新 snapshot:snapshot文件会自动更新,测试即可通过~

~snapshot test 反思~

如果你看了上一章节,便可知道我不建议写snapshot测试,上面介绍snapshot是因为它在Enzyme中很普遍,下面我还会解释为何不用这种测试方法。

让我们来回顾一下snapshot测试,它显示了组件与之前有何变化,它的好处是:

  1. 写snapshot测试很简单,有时只需要简单的几行代码。
  2. 能够看出组件渲染的结果,用.debug()清楚地看DOM节点。

~为什么不建议用snapshot testing~

  1. 这种测试方法只告诉你code的语法有哪些变更。
  2. 所以它到底测试了什么呢?
  3. 因为测试时渲染了React的APP,所以他也会测试一部分第三方库的功能。
  4. Diff也是Git的功能,所以snapshot 测试Diff不是很必要了
  5. 失败的snapshot测试并不说明功能一定出错,它代表视图是否有了变化。报错后执行了u更新snapshot后,开发者也不会仔细去看每个Diff的细节。
  6. snapshot testing还能显示语法的错误,可我们在开发时控制台就能调试语法错误。所以这个功能也没有太大的意义了。
  7. 许多人的snapshot测试使用的方法是 shallow rendering ,这种不渲染子组件的测试结果没有让开发者看到深层次的问题。


使用Enayme,测试功能的细节

下面我来举个反例,说明为何不对功能具体实现的细节进行测试。假如,我们有个简单的计数器组件

import React, { Component } from 'react';

class Counter extends Component {
  constructor(props) {
    super(props)

    this.state = {
      count: 0
    }
  }

  increment = () => {
    this.setState({count: this.state.count + 1})
  }

  //This incorrect code will still cause tests to pass
  // <button onClick={this.incremen}>
  //   Clicked: {this.state.count}
  // </button>

  render() {
    return (
      <div>
        <button className="counter-button" onClick={this.incremen}>
          Clicked: {this.state.count}
        </button>
      </div>
  )}
}

export default Counter;

你能注意到吧,上面有一条注释,即使函数名拼错了,但这个测试依然能够通过。

👇下面我们用test来解释原因。

import React from 'react';
import ReactDOM from 'react-dom';
import Counter from '../counter';

import Enzyme, { shallow, render, mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() })

// incorrect function assignment in the onClick method
// will still pass the tests.

test('the increment method increments count', () => {
  const wrapper = mount(<Counter />)

  expect(wrapper.instance().state.count).toBe(0)

  // wrapper.find('button.counter-button').simulate('click')
  // wrapper.setState({count: 1})
  wrapper.instance().increment()
  expect(wrapper.instance().state.count).toBe(1)
})

上面这个例子虽然通过了测试,但我没有信心,当用户使用APP的时候是没有问题的。

每当我们改了函数名、CSS类名的时候我们需要重写测试,这种体验太糟糕了,怎么解决呢?


React-testing-library

~useState~

测试越接近用户的行为,你会对测试的结果越有信心。
—— react-testing-library docs

接下来的测试,我们也遵循这个原则,“不断接近用户的行为编写测试”。

👇下面用React Hooks组件来测试state和props。

import React, { useState } from 'react';

const TestHook = (props) => {
  const [state, setState] = useState("Initial State")

  const changeState = () => {
    setState("Initial State Changed")
  }

  const changeNameToSteve = () => {
    props.changeName()
  }

  return (
  <div>
    <button onClick={changeState}>
      State Change Button
    </button>
    <p>{state}</p>
    <button onClick={changeNameToSteve}>
       Change Name
    </button>
    <p>{props.name}</p>
  </div>
  )
}

export default TestHook;

props来自父组件

const App = () => {
      const [state, setState] = useState("Some Text")
      const [name, setName] = useState("Moe")
  ...
      const changeName = () => {
        setName("Steve")
      }

      return (
        <div className="App">
         <Basic />
        <h1> Counter </h1>
         <Counter />
        <h1> Basic Hook useState </h1>
         <TestHook name={name} changeName={changeName}/>
    ...     

现在回想一下我们的测试原则:“模仿用户的行为”,然后,我们的测试怎么写呢?

设想一下用户的交互过程:看到了UI上的文字,看到了按钮的文字,点击按钮,UI上面文字更新了。

我们用React-testing-library测试上面的例子。

~安装~

npm install @testing-library/react

而不是执行 npm install react-testing-library

~测试例子解读~

import React from 'react';
import ReactDOM from 'react-dom';
import TestHook from '../test_hook.js';
import {render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'

afterEach(cleanup)

it('Text in state is changed when button clicked', () => {
    const { getByText } = render(<TestHook />);

    expect(getByText(/Initial/i).textContent).toBe("Initial State")

    fireEvent.click(getByText("State Change Button"))

    expect(getByText(/Initial/i).textContent).toBe("Initial State Changed")
 })


it('button click changes props', () => {
  const { getByText } = render(<App>
                                <TestHook />
                               </App>)

  expect(getByText(/Moe/i).textContent).toBe("Moe")

  fireEvent.click(getByText("Change Name"))

  expect(getByText(/Steve/i).textContent).toBe("Steve")
})
  1. 引入依赖项
  2. afterEach(cleanup) 每个test之后我们需要unmount或者 cleanup ,因为我们选择的不是shallow render。
  3. getByText 是我们从 render 函数得到的 object中解构出来的查询函数 。虽然还有其他的类似查询函数,但它用得最多。
  4. 在测试state的时候,我们没用任何函数名或者state变量的名字。时刻记着“模仿用户行为,不测试实现细节”的原则。用户看到UI的文字,我们会去检索DOM节点的文字,我们也会用类似的方式检索按钮并点击它。再检索文字的变化。
  5.  (/Initial/i) 是正则表达式,它返回第一个包含Initial的元素。
  6. 对于 props的测试方法也类似。当 props要在 App.js 中改变时,组件要一起渲染出来。同时,注意用👆“模仿用户行为”的原则写测试。

希望上面介绍的方法能帮助你学会用React-testing-library和“测试原则”写测试,下一篇文章我们来看看一些例外的情况。


感谢阅读