知道如何测试是伟大而重要的。我已经创建了很多内容,教人们测试的基本原理,如何配置工具,如何为特定场景编写测试,等等。但是,知道如何编写测试仅仅是实现对你的应用程序的信心的一半。知道要测试什么才是另一个非常重要的战斗的一半。
在我的研讨会材料和TestingJavaScript.com上,我确实谈到了如何知道要测试什么,但我被问及这个问题时,我认为写一篇关于这个问题的博文会很好。所以在这里你可以看到
**我们写测试是为了确信我们的应用程序在用户使用时能够工作。**有些人写测试也是为了加强他们的工作流程,这很好,但我最终对信心感兴趣。既然如此,我们的测试应该直接映射到增强我们的信心。这里是我希望你在写测试时考虑的关键点。
少考虑你要测试的代码,多考虑代码支持的用例。
当你考虑代码本身的时候,开始测试实现细节是很容易和自然的(这是通往灾难的道路)。
思考用例可以使我们更接近于以用户使用应用程序的方式来编写测试。
代码覆盖率是一个指标,它显示了我们的代码在测试中被运行的行数。让我们用这段代码作为一个例子。
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
现在,我们没有对这个函数进行测试,所以我们的代码覆盖率报告会显示,我们对这个函数的覆盖率是0% 。在这种情况下,代码覆盖率报告帮助我们了解到需要进行测试,但它并没有告诉我们这个函数的重要性,也没有告诉我们这个函数所支持的用例,而这正是我们在编写测试时要考虑的最重要的因素。
事实上,当考虑到整个应用程序并想知道要测试什么时,覆盖率报告在让我们了解我们应该把大部分时间花在哪里方面做得很差。
所以覆盖率报告可以帮助我们识别代码库中哪些代码缺少测试。因此,当你看到代码覆盖率报告并注意到缺少测试的行时,不要考虑ifs/elses、循环或生命循环。而是问问自己。
这些代码行支持什么用例,我可以添加什么测试来支持这些用例?
"用例覆盖率 "告诉我们,我们的测试支持多少个用例。不幸的是,没有自动的 "用例覆盖率报告 "这回事。我们必须自己做这个。但代码覆盖率报告有时可以帮助我们识别我们没有覆盖的用例。让我们试试吧。
所以,如果我们阅读代码并考虑一分钟,我们就可以确定我们要支持的第一个用例。"如果给定一个数组,它将返回一个数组"。这个用例声明实际上是我们测试的一个很好的标题。
test('returns an array if given an array', () => {
expect(arrayify(['Elephant', 'Giraffe'])).toEqual(['Elephant', 'Giraffe'])
})
有了这个测试,我们的覆盖率报告看起来是这样的(高亮的行被覆盖)。
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
现在,我们可以看看剩下的几行,确定还有两个用例我们的测试还不支持。
- 如果给定一个错误的值,它会返回一个空数组
- 如果不是数组,也不是falsy,它返回一个带有给定参数的数组。
让我们为这些用例添加测试,看看它对代码覆盖率有什么影响。
test('returns an empty array if given a falsy value', () => {
expect(arrayify()).toEqual([])
})
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
很好,就快完成了!
test(`returns an array with the given argument if it's not an array and not falsy`, () => {
expect(arrayify('Leopard')).toEqual(['Leopard'])
})
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
很好!现在我们可以确信,只要我们不需要改变这个函数的用例,我们的测试将继续通过。
代码覆盖率并不是一个完美的指标,但它可以成为一个有用的工具,来确定我们的代码库中哪些部分缺少 "用例覆盖"。
有时,我们的代码覆盖率报告显示100%的代码覆盖率,但不是100%的用例覆盖率。这就是为什么有时我试图在开始写测试之前就想好所有的用例。
例如,让我们想象一下,arrayify 函数是这样实现的:
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else {
return [maybeArray].filter(Boolean)
}
}
这样一来,我们就可以通过以下两个用例获得100%的覆盖率:
- 如果给定一个数组,它返回一个数组
- 如果不是数组,则返回一个带有给定参数的数组。
但如果我们能看一下用例覆盖率报告,就会发现我们缺少这个用例:
- 如果给定一个错误的值,它会返回一个空数组
这可能是坏事,因为现在我们的测试没有给我们那么大的信心,当用户像这样使用我们的代码时,我们的代码会工作:arrayify() 。现在,这很好,因为即使我们没有测试,我们的代码也支持这种用例。但我们有测试的原因是为了确保代码继续支持我们想要支持的用例,即使事情发生变化。
因此,举个例子说明缺少这个测试会出什么问题,有人会来这里,看到那个.filter(Boolean) ,然后想:"哼,这很奇怪......我不知道我们是否真的需要这个。我想知道我们是否真的需要这个。"因此,他们删除了它,而我们的测试继续通过,但任何依赖虚假行为的代码现在都被破坏了。
关键的启示。
测试用例,而不是代码。
在编写代码时,记住你已经有两个需要支持的用户。终端用户,和开发者用户。同样,如果你考虑的是代码而不是用例,那么开始测试实现细节就会变得很危险。当你这样做的时候,你的代码现在有了第三个用户。
下面是人们经常考虑测试的React的几个方面,这导致了实施细节测试。对于所有这些,与其考虑代码,不如考虑代码对终端用户和开发者用户的可观察效果,这是你的用例,测试这个:
- 生命周期方法
- 元素事件处理程序
- 内部组件状态
相反,这里是你应该测试的东西,因为它们关系到你的两个用户。每一个都可以改变DOM,发出HTTP请求,调用回调道具,或执行任何其他数量的可观察的副作用,这对测试是很有用的。
- 用户互动(使用
userEvent来自@testing-library/user-event)。终端用户是否能够与组件所呈现的元素进行互动? - 改变道具(使用
rerender来自React测试库)。当开发者用户用新的道具重新渲染你的组件时会发生什么? - 上下文变化(使用
rerenderfrom React Testing Library)。当开发者用户改变上下文导致你的组件重新呈现时会发生什么? - 订阅变化。当组件订阅的事件发射器发生变化时会发生什么?(比如Firebase、redux商店、路由器、媒体查询,或者基于浏览器的订阅,比如在线状态)。
所以我们知道如何考虑对我们应用的单个组件甚至是页面进行测试,但你从哪里开始呢?这有点让人不知所措。特别是如果你刚刚开始在一个大型应用程序中进行测试。
所以你要做的是,从用户的角度考虑你的应用程序,然后问。
如果这个应用的哪个部分坏了,会让我最难过?
或者,更普遍的是。
这个应用程序中最糟糕的故障是什么?
我建议列出你的应用程序所支持的功能清单,并根据这个标准对它们进行优先排序。这是一个很好的练习,可以和你的团队和经理一起做。这个会议的副作用是帮助房间里的每个人理解测试的重要性,并希望能说服他们,在你需要做的所有其他功能工作中,它应该得到一定程度的优先考虑。
一旦你有了这个优先列表,我建议写一个单一的端到端(E2E)测试,以涵盖大多数用户在特定用例中的 "快乐路径"。通常情况下,你可以通过这种方式覆盖列表中几个最重要的功能的一部分。这可能需要一点时间来设置,但它会给你带来巨大的收益。
E2E测试不会给你100%的用例覆盖率(你甚至不应该尝试),也不会给你100%的代码覆盖率(你甚至不应该记录E2E测试),但它会给你一个伟大的起点,并大大增强你的信心。
一旦你有了一些E2E测试,那么你就可以开始考虑为一些E2E测试中缺少的边缘案例编写一些集成测试,为这些功能使用的更复杂的业务逻辑编写单元测试。从这里开始,只是随着时间的推移增加测试的问题。不要把目标放在100%的代码覆盖率报告上,这不值得花时间。
关于建立测试文化和合理的代码覆盖率目标,我建议观看Aaron Abramov在AssertJS 2018的演讲。用软件设计原则建立测试模式
在这里阅读更多关于不同类型的测试之间的区别。前端应用程序的静态测试与单元测试与集成测试与E2E测试
如果有足够的时间和经验,你会形成一种直觉,知道要测试什么。你可能会犯错,也会有一些挣扎。不要放弃!继续前进。好运。