大多数开发人员不知道如何测试
每个开发人员都知道我们应该编写单元测试,以防止将缺陷部署到生产中。
大多数开发人员不知道的是每个单元测试的基本要素。我无法开始计算我看到单元测试失败的次数,完全不知道开发人员试图测试什么功能,更不用说它出了什么问题或为什么重要。
在我最近的一个项目中,我们让一大片单元测试进入测试套件,但完全没有描述测试的目的。我们有一个很棒的团队,所以我放松了。结果?只有作者才能真正理解的大量单元测试。
幸运的是,我们完全重新设计了API,我们将把整个套件扔掉并从头开始 - 这将是我修复列表中的优先级#1。
不要让这件事发生在你身上。
为什么进行测试?
您的测试是防范软件缺陷的第一道防线。您的测试比linting和静态分析更重要(它只能找到错误的子类,而不是实际程序逻辑的问题)。测试与实现本身一样重要(重要的是代码满足要求 - 如果实现得不好,它的实现方式根本不重要)。
单元测试结合了许多功能,使它们成为应用程序成功的秘密武器:
- 设计辅助:首先编写测试可以让您更清晰地了解理想的API设计。
- 功能文档(面向开发人员):测试描述在代码中包含每个实现的功能要求。
- 测试开发人员的理解:开发人员是否足够理解问题,以便在代码中阐明所有关键组件的要求?
- 质量保证:手动QA容易出错。根据我的经验,开发人员在更改重构,添加新功能或删除功能后,无法记住需要测试的所有功能。
- 持续交付援助:自动化质量保证提供了自动防止破坏的构建部署到生产的机会。
单元测试不需要扭曲或操纵来满足所有这些广泛的目标。相反,单元测试的基本性质是满足所有这些需求。这些好处都是良好编写的具有良好覆盖率的测试套件的副作用。
TDD(测试驱动开发)
有证据说:
- TDD可以降低错误密度。
- TDD可以鼓励更多模块化设计(提高软件灵活性/团队速度)。
- TDD可以降低代码复杂性。
科学说:大量的经验证据表明TDD有效**。
先写测试
Microsoft Research,IBM和Springer tested 优先测试与后测试方法的效果,并始终发现优先测试处理比后来添加测试产生更好的结果。它非常明确:在您实施之前,请编写测试。
在实施之前,写测试。
什么是良好的单元测试?
好的,所以TDD有效。首先编写测试。要更有纪律。相信这个过程......我们明白了。但是你怎么写一个好的单元测试?
我们将从一个真实的项目中看一个非常简单的例子来探索这个过程:来自Stamp Specification的compose()
函数。
我们将use tape进行测试,因为它有着透明度和简洁性。
在我们回答如何编写良好的单元测试之前,首先我们必须了解如何使用单元测试:
- 设计辅助:在设计阶段,在实施之前编写。
- 功能文档和开发人员理解测试:测试应提供正在测试的功能的清晰描述。
- 质量保证/持续交付:测试应在故障时停止交付管道,并在失败时生成错误报告。
单元测试作为Bug报告
当测试失败时,该测试失败报告通常是您关于确切出错的第一个也是最好的线索 - 快速追踪根本原因的秘诀就是知道从哪里开始寻找。当您有一个非常明确的错误报告时,这个过程会变得更加容易。
失败的测试应该读起来像高质量的bug报告。
好的测试失败bug报告中包含什么?
- 你在测试什么?
- 它应该做什么?
- 输出是什么(实际)?
- 预期输出(预期行为)是什么??
良好故障报告的示例
首先回答“你在测试什么?”:
- 您正在测试哪些组件方面?
- 该功能应该做什么?您正在测试哪些特定的行为要求?
The compose()
function takes any number of stamps (composable factory functions) and produces a new stamp.
compose()
函数应该测试的内容,并产生相应的结果。
要编写此测试,我们将从任何单个测试的最终目标向后工作:测试特定的行为要求。为了使这个测试通过,代码必须产生什么特定的行为?
该功能应该做什么?
我喜欢从写一个字符串开始。没有分配给任何东西。没有传递给任何函数。只需明确关注组件必须满足的特定要求。在这种情况下,我们将从compose()
函数返回一个函数开始。
一个简单,可测试的要求:
'compose()应该返回一个函数。'
现在我们将跳过一些内容并充实其余的测试。这个字符串是我们的目标。事先说明它有助于我们关注奖品。
我们测试的组件方面是什么?
“组件方面”的含义因测试而异,具体取决于为测试组件提供足够覆盖所需的粒度。
在这种情况下,我们将测试compose()
函数的返回类型,以确保它返回正确类型的东西,而不是undefined
或什么都没有,因为它在你运行它时抛出。
让我们把这个问题转换成测试代码。答案出现在测试描述中。这一步也是我们调用函数并传递回调函数的地方,当测试运行时,测试运行器将调用这个回调函数:
test('
', 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 = '
';
const expected = '';
第一个问题确实在测试失败中起到双重作用。通过回答这个问题,您的代码也会回答另一个问题:
const actual = '
';
需要注意的是很重要的actual
*值必须通过行使某些组件的公共API的生产。否则,测试没有价值。我已经看到测试套件如此淹没了模拟和存根以及铃声和口哨,一些测试从未运用任何据称正在测试的代码。
让我们回到这个例子:
const actual = typeof compose();
const expected = 'function';
你可以构建一个断言,而不是专门为名为actual
和expected
的变量赋值,但我最近开始专门为每个测试中的actual
和expected
变量赋值,发现它使我的测试更容易读。
看看它如何澄清断言?
assert.equal(actual, expected,
'compose() should return a function.');
它将“如何”与测试体中的“什么”分开。
- 想知道我们如何得到结果?查看变量赋值。
- 想知道我们正在测试什么?看一下断言的描述。
结果是测试本身的读取就像高质量的错误报告一样容易。
让我们看一下上下文中的所有内容:
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();
});
下次编写测试时,请记住回答所有问题:
- 你在测试什么?
- 它该怎么办?
- 什么是实际的输出?
- 什么是预期的输出?
- 如何复制测试?
最后一个问题由用于派生“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();
});
使用单元测试还有很多,但知道如何编写一个好的测试还有很长的路要走。