大多数开发人员不知道如何进行测试
每个开发人员都知道我们应该编写单元测试,以防止缺陷被部署到生产环境中。
大多数开发人员不知道的是每个单元测试的基本要素。我无法数清我看到过多少次单元测试失败,只是为了调查并发现我完全不知道开发人员试图测试哪个功能,更不用说它出了什么问题或为什么它很重要。
在我的一个最近的项目中,我们让一大堆单元测试进入测试套件,但是完全没有描述测试的目的。我们有一个很棒的团队,所以我放松了警惕。结果呢?我们仍然有很多单元测试,只有作者才能真正理解它们。
幸运的是,我们正在彻底重新设计API,我们将扔掉整个测试套件并从头开始,否则,这将是我修复列表上的第一优先级。
不要让这种情况发生在你身上。
为什么要注重测试纪律?
你的测试是防止软件缺陷的第一道防线。你的测试比linting和静态分析更重要(它们只能找到错误的子类,而不能找到实际程序逻辑的问题)。测试与实现本身一样重要(只要代码满足要求即可,它的实现方式根本不重要,除非实现得很差)。
单元测试结合了许多功能,使它们成为应用程序成功的秘密武器:
- 设计帮助: 先编写测试可以让你更清晰地了解理想的API设计。
- 特性文档(面向开发人员): 测试描述在代码中记录了每个实现的特性要求。
- 测试开发人员的理解: 开发人员是否足够了解问题,以在代码中表达所有关键组件要求?
- 质量保证: 手动QA容易出错。根据我的经验,开发人员在重构、添加新功能或删除功能后记得所有需要测试的功能是不可能的。
- 持续交付支持: 自动化QA提供了自动防止部署到生产环境的错误构建的机会。 单元测试不需要扭曲或操纵来满足所有这些广泛的目标。相反,单元测试的本质性质就是满足所有这些需求。这些好处都是良好编写测试套件和良好覆盖率的副作用。
TDD的科学
证据表明:
- TDD可以减少缺陷密度。
- TDD可以鼓励更模块化的设计(增强软件敏捷性/团队速度)。
- TDD可以减少代码复杂度。
科学证明: 有_重要的经验证据表明TDD有效_。
先编写测试
来自微软研究、IBM和Springer的研究测试了测试优先和测试后方法的功效,并始终发现测试优先的过程比后来添加测试产生更好的结果。这是非常明确的:在实现之前,先编写测试。
在实现之前,编写测试。
一个好的单元测试中包含什么?
好的,所以TDD有效。先编写测试。更加纪律。相信过程……我们懂了。但是,如何编写一个好的单元测试?
我们将从一个真实项目中的非常简单的例子中探索过程:Stamp Specification中的_compose()_函数。
我们将使用tape进行测试,因为它非常清晰和基本简单。
在回答如何编写好的单元测试之前,我们必须首先了解单元测试的使用方式:
- 设计帮助: 在设计阶段编写,在实现之前。
- 特性文档和开发人员理解的测试: 测试应提供清晰的功能描述。
- QA/持续交付: 测试应在失败时停止交付流水线,并在失败时生成良好的错误报告。
单元测试作为错误报告
当测试失败时,测试失败报告通常是关于出了什么问题的第一个和最好的线索,快速追踪根本原因的秘密是知道从哪里开始查找。当你有一个非常清晰的错误报告时,这个过程会变得更加容易。
一个失败的测试应该读起来像一个高质量的错误报告。
一个好的测试失败报告中包含什么?
- 你在测试什么?
- 它应该做什么?
- 输出是什么(实际行为)?
- 期望的输出是什么(期望的行为)?
一个好的失败报告示例。
从回答“你在测试什么?”开始:
- 你在测试什么组件方面?
- **它应该做什么?**你测试了哪个特定的行为要求?
_compose()_函数接受任意数量的邮票(可组合的工厂函数)并生成一个新的邮票。
为了编写这个测试,我们将从任何单个测试的最终目标开始:测试特定的行为要求。为了使此测试通过,代码必须产生什么具体行为要求?
它应该做什么?
我喜欢先写一个字符串。不分配给任何东西。不传递到任何函数。只是清晰地关注组件必须满足的特定要求。在这种情况下,我们将从_compose()_函数应该返回一个函数开始。一个简单可测试的需求:
'compose() should return a function.'
现在我们将跳过一些内容,完善测试的其余部分。这个字符串是我们的目标。事先说明可以帮助我们保持注意力集中在目标上。
我们正在测试哪个组件方面?
“组件方面”的意思会因测试而异,这取决于为了充分覆盖正在测试的组件所需的粒度。
在这种情况下,我们将测试 compose()
函数的返回类型,以确保它返回正确的类型,而不是 undefined
或根本什么都没有,因为当你运行它时它会抛出错误。
让我们将这个问题翻译成测试代码。答案放在测试描述中。这一步也是我们将调用函数并传递测试运行器在运行测试时调用的回调函数的地方:
test('<What component aspect are we testing?>', assert => {
});
在这种情况下,我们正在测试组合函数的输出:
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';
你可以构建一个断言,而不是专门为名为 actual
和 expected
的变量分配值,但我最近开始在每个测试中专门为名为 actual
和 expected
的变量分配值,并发现这使我的测试更容易阅读。
看看它如何澄清断言?
assert.equal(actual, expected,
'compose() should return a function.');
它将测试主体中的“如何”与“什么”分开。
- 想知道 我们是如何得出结果的? 查看 变量赋值。
- 想知道 我们正在测试什么? 查看 断言的描述。
结果是,测试本身读起来就像一个高质量的 bug 报告。
让我们看看整个上下文:
下次你写测试时,记得回答所有的问题:
- 你正在测试什么?
- 它应该做什么?
- 实际输出是什么?
- 期望输出是什么?
- 测试如何重现?
最后一个问题通过用于推导
actual
值的代码来回答。
一个单元测试模板:
使用单元测试的技巧远不止这些,但知道如何编写一个好的测试会有很大的帮助。
译自:https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d
评论(0)