每个单元测试必须解决的 5 个问题

458 阅读8分钟

原文地址: medium.com/javascript-…
译文地址:github.com/xiao-T/note…
本文版权归原作者所有,翻译仅用于学习。


大多开发者不知道如何测试

为了避免对生产环境带来影响,每个开发者都知道需要编写单元测试。

大多数开发者并不知道每个单元测试的基本组成部分。我不记得见过多少次失败的单元测试,调查发现我无法理解开发者想测试什么,更不用说,它是如何错误的,或者为什么那么重要。

在我最近的一个项目中,我引入了大量的,没有任何描述说明的测试代码。我们是一个庞大的团队,因此,我放松了警惕。结果呢?我们有大量的单元测试只有对应的开发者自己明白干了什么。

很幸运,我们完全重新设计了 API,我们完全抛弃了之前的方案,从头开始;也就是说,这将是我最重要的事情。

不要让这些事情发生在你身上。

为什么要单元测试呢?

测试是抵御软件缺陷的第一道防线,也是最基本的防线。测试比 Lint 和静态分析(它们只能发现少量的错误,并不能发现程序中的逻辑问题)更加重要。测试和实现需求一样重要(最重要的是代码是否满足需求,除非实现的非常差,如何实现并不重要)。

单元测试包含了很多功能,这使它成了应用成功的秘密武器:

  1. **辅助设计:**编写单元测试可以让你设计出更加清晰理想的 API。

  2. **功能文档(针对开发者):**测试中包含了所有的功能需求。

  3. **测试开发者对需求的理解:**开发者是否对所有的需求有足够的了解?以便实现更好的代码。

  4. **质量保障:**手动 QA 容易出错。以我的经验,在重构、添加新功能或者移除功能后,开发者不能记住所有需要测试的功能点。

  5. **辅助持续交付:**自动 QA 可以自动的避免破环生产环境。

单元测试并不是通过扭曲或者操纵来达到某些目的。相反,它本质上是为了满足需求。良好的单元测试可以带来更好的测试覆盖率。

TDD 科学

证据表明:

  • TDD 可以减少 bug 的密度
  • TDD 可以促进更好的模块化设计(提升软件的敏捷性/团队效率)
  • TDD 可以减少代码的复杂度

科学证明: 有大量的经验证明 TDD 带来的好处

测试优先

来自微软、IBM 和 Springer 研究证明,一致的认为测试优先比延后测试可以产出更好的结果。现在非常的明确:在具体实现功能之前,需要先编写单元测试。

实现具体功能之前,

首先,需要编写单元测试

良好的单元测试应该是什么样子呢?

好,遵循 TDD 规则。先写测试。更规范。相信它...我知道了。但是如何编写一个良好的单元测试呢?

我们通过一个真实的场景来逐步探索:来自 Stamp 规范compose() 方法

我们将会使用tape作为测试框架,因为它非常清晰简单。

在回答如何编写良好单元测试之前,首先,我们需要明白如何使用单元测试:

  • **辅助设计:**在设计阶段编写测试,也就是说功能实现之前
  • **功能文档 & 测试开发者对需求的理解:**单元测试应该对所测试的功能有一个清晰的描述
  • **QA/持续交付:**单元测试在交付过程中失败的话,应该立即停止交付,并提供一个良好的 bug 报告

单元测试就是一个 Bug 报告

每当测试失败,失败报告都是有关错误的最好的线索 — 快速跟踪根本原因的秘密就是知道从哪开始。如果,你有一个清晰 bug 报告,这个过程会让你更加容易。

一个失败的单元测试看起来应该像是一个高质量的 bug 报告。

一个良好的错误报告应该包含哪些内容?

  1. 测试了什么内容?

  2. 应该是什么?

  3. 输出了什么(真实的行为)?

  4. 期望输出什么(期望的行为)?

开始回答:测试了什么内容?

  • 你在测试组件的哪些内容
  • **功能应该是什么样的?**哪些特殊的行为需求你需要测试?

可以传递给函数 compose() 多个 stamps,然后,会产出一个新的 stamp。

为了编写测试用例,我们需要逆向思考:测试特定的行为需求。为了让测试通过,代码应该提供什么样的行为?

功能应该是什么样的?

我喜欢从一个字符串开始。不分配任何内容。不传递给任何函数。只是明确组件需要满足的特定功能需求。在这个示例中,我们将从 compose() 会返回一个函数这一事实开始。

一个简单的测试需求:

'compose() should return a function.'

现在, 我们需要跳过一些东西,并且完善剩余的测试内容。这个字符串的内容就是我们的目的。事先说明它,这有助于我们更加关注功能需求。

我们需要测试组件哪些内容?

你所说的“组件内容”因测试而异,具体取决于测试组件所属的覆盖率。

在这个示例中,我们需要测试函数 compose() 运行后返回的类型,确保它返回了正确的内容,而不是 undefined 或者什么都没有。

让我们把这些问题用代码来描述。用测试来回答。这一步我们会调用我们的函数,然后,传递一个回调函数,测试运行时会调用这个回调函数:

test('<What component aspect are we testing?>', assert => {
});

这个示例,我们将会测试函数 compose 的输出:

test('Compose function output type.', assert => {
});

当然,我们还是需要一个描述。它在回调函数中:

test('Compose function output type.', assert => {
  'compose() should return a function.'
});

输出什么内容(期望输出和真实输出)?

equal() 是我最喜欢的断言方法。如果,测试框架中唯一有效的断言就是 equal(),那么,每个测试框架都会更好。为什么?

这是因为,equal() 可以很自然的回答单元测试必须回答的两个最重要的问题,但是,大多数并没有:

  • 真实输出的内容是什么?
  • 期望输出的内容是什么?

如果,你的测试中没有回答这两个问题,这说明你的单元测试并不合格。你做了一个草率的、不成熟的测试。

如果,你用一句话总结这篇文章,那么就是:

Equal 是你默认的断言。

它是每个测试框架的主要的内容。

所有的高级断言框架有着数百种不同的断言,这正是消弱单元测试的原因所在。

挑战

是否想要编写更好的单元测试?接下来的几周,编写单元测试时尝试着只用 equal() 或者 deepEqual(),或者你所用测试框架中相等的断言。不要担心这对测试质量的影响,我敢保证这将会极大的改善你的测试用例

这些代码看起来像什么?

  const actual = '<what is the actual output?>';
  const expected = '<what is the expected output?>';

第一个问题作为测试具有双重责任。为了回答这个问题,你同时还需要回到另外一个问题:

  const actual = '<how is the test reproduced?>';

需要注意的是:**actual 的值必须是通过组件的某些公共 API 产出的。**否则,测试就没有意义。我看到过很多的测试案例里面充斥各种花里胡哨的内容,但是,并没有达到测试的效果。

我们回到示例中:

const actual = typeof compose();
const expected = 'function';

你可以构建一些断言,但是,并不一定非得把它们命名为 actualexpected,但是,在每个测试中我开始把这些变量命名为 actualexpected,然后,我发现这让我的测试代码更容易阅读。

看一下这个断言多么的清晰?

assert.equal(actual, expected,
    'compose() should return a function.');

它将测试中的 “how” 和 “what” 分离开来。

  • 想知道**结果是什么?**看一下变量对应的值。
  • 想知道**我们需要测试什么?**看一下断言内容。

这样测试的结果就像一个高质量的 bug 报告,更加容易阅读。

我们来看一下完整的内容:

import test from 'tape';
import compose from '../source/compose';

test('Compose function output type', assert => {
  const actual = typeof compose();
  const expected = 'function';

  assert.equal(actual, expected,
    'compose() should return a function.');

  assert.end();
});

下次,你在写测试的时候,记住以下这几个问题:

  1. 你需要测试什么?

  2. 应该怎么做?

  3. 真正输出的内容是什么?

  4. 期望输出的内容是什么?

  5. 如何重现测试?

最后一个问题可以通过代码得到 actual 的值来回答。

单元测试模版:

import test from 'tape';

// For each unit test you write,
// answer these questions:
test('What component aspect are you testing?', assert => {
  const actual = 'What is the actual output?';
  const expected = 'What is the expected output?';

  assert.equal(actual, expected,
    'What should the feature do?');

  assert.end();
});

有很多方式可以编写良好的单元测试,但是,如何编写良好的单元测试还有很长的路要走。

下一步

加入 TDD Day 可以观看很多有关 TDD 的直播课程。TDD Day 可以带领你的团队了解更多有关 TDD 的高级技能。学习各种测试及其作用,如何编写可测试的软件,以及 TDD 如何使我成为更好的开发者,以及如何让你受益。

通过网络直播可以观看有关 ES6 & React 的 TDD 内容

更多内容在 Lifetime Access Pass