单元测试的陷阱

写 vue 的单元测试时应该避免的 5 个陷阱

陷阱1: 等到最后才考虑单元测试应该怎么写

高手应该在写代码时就考虑单元测试应该怎么写, 这样写出来的代码才是易于测试的.

陷阱2: 测试了不该测试的内容

测试的一个很重要的功能是: 帮助你重构代码. 如果重构了你的代码, 导致你需要重构测试用例. 说明你大概率进行了错误的测试.

一个错误的测试用例如下:

import { shallow } from 'vue-test-utils';
import ExampleComponent from '../ExampleComponent.vue';
import Sinon from 'sinon';

describe('Bad Example', () => {
  it('Do not do this: should toggle the visibility', () => {
    const toggleParagraphDisplaySpy = Sinon.spy(
      ExampleComponent.methods,
      'toggleParagraphDisplay'
    );

    const wrapper = shallow(ExampleComponent);
    wrapper.find('button').trigger('click');

    expect(toggleParagraphDisplaySpy.calledOnce);

    toggleParagraphDisplaySpy.restore();
  });
});

这个测试里, 我过多的关注了实现的细节, 测试用例和代码实现耦合度高, 当我需要重构比如更改了函数名称时, 我既要更改组件里的函数名称, 又需要改测试用例的内部语句.

icymind: 如果不这样, 那该怎么测? 毕竟一个函数调用另外的函数是太正常不过的.
解惑: 应该测试的是组件的公开接口和副作用

PS: 另一个 幻灯片 提到了哪些是不应该测试的(作者github: @matt-oconnell): - 实现的细节 - 框架提供的功能(我们应该信任框架, 框架的功能应该由框架自身去测试) - 比如不应该测试 props 被渲染 - 不应该测试 props 的校验

测试公开接口

组件的接口是:

  • props
  • user interaction
  • lifecycle methods

它的输出是

  • events
  • rendered output

所以测试公开接口其实就是

  • 某个事件被触发
  • 测试某个类名(不)应该出现
  • 测试某个元素(不)出现在渲染接口
  • 子组件(不)应该被渲染

测试副作用

副作用包含 http 请求, 更改数据库, 更改文件系统, 更改页面元素(疑惑: 页面元素到底是组件的输出还是组件的副作用?)

陷阱3: 测试替身(Test Doubles)

测试替身从简单到复杂:

  • dummy
    dummy 是个空方法, 不做任何事. 主要用于解决测试用例中编译不通过的问题.

  • stub
    stub 和 dummy 类似, 它会返回设计好的值.

  • spy
    spy 和 dummy 的区别是, spy 可以记住函数的执行情况(如执行次数/执行参数等)

  • mock
    mock 和 spy 类似, 此外 mock 还有一个 verify 方法, 它可以检查代码可以按照预期的顺序执行.

  • fake
    fake 是一个假的实现, 可以用于模拟副作用, 在集成测试中非常有用

icymind: 作者的这个理解和 jest 框架提供的功能有较大差距, 需要进一步了解 jest 提供的各类测试替身

陷阱4: 结构的耦合

测试用例文件的组织不应该和代码文件的组织高度耦合, 否则更改代码的位置, 我们需要在测试用例文件中作出相应的调整, 这会拖慢效率.

陷阱5: 测试一切东西

如果妄图测试一切东西, 那么模拟外部环境将会变得非常复杂. 更可取的做法是: - 子组件保持纯粹, 尽可能避免副作用, 不发 http 请求, 不写数据库, 不依赖除 props 之外的变量. 如此子组件将易于测试. - 父组件尽量不含逻辑, 只负责从外部环境(服务器, 数据库, 全局变量)获取数据, 然后将数据传给子组件. 因为父组件不含逻辑, 所以不需要测试.