我发现有人告诉我他们不觉得单元测试有用,而且通常他们编写的测试不好。这是完全可以理解的,特别是如果您不熟悉单元测试。编写好的测试很难需要练习才能达到。我们今天要谈论的所有事情都是通过艰苦的方式学到的;糟糕的单元测试带来的痛苦让我创建了自己的规则来编写一个好的单元测试。我们今天要讨论的就是这些规则。

为什么糟糕的测试如此糟糕?

当您的应用程序代码凌乱时,很难使用。但希望你有一些测试,这些测试可以帮助你。如果您有测试支持,则可以使用硬代码。置信度测试使您可以消除不良代码的影响。

糟糕的测试没有任何代码可以帮助您使用它们。你不会为你的测试编写测试。你可以,但是你必须为你的测试编写测试,这是一个螺旋式的,我们谁都不想走下去……

不良测试的特征

很难定义一组构成糟糕测试的特征,因为糟糕的测试实际上是任何不遵循我们将要讨论的规则的测试。

如果你曾经看过一个测试并且不知道它在测试什么,或者你不能明显地发现断言,那是一个糟糕的测试。描述写得不好的测试(​​it('works')​​是个人最喜欢的)是一个糟糕的测试。

如果您发现它们没有用,那么测试就是不好的。进行测试的全部目的是提高您的生产力、工作流程和对代码库的信心。如果测试没有做到这一点(或积极使情况变得更糟),那么这是一个糟糕的测试。

我坚信,糟糕的测试比没有测试更糟糕。

一个好的测试从一个好名字开始

好消息是,好的测试规则很容易记住,而且一旦你习惯了它们就非常直观!

一个好的测试有一个简洁、描述性的名称。如果您想不出一个简短的名称,请选择清晰而不是节省行长。

it('filters products based on the query-string filters', () => {})

您应该能够仅从描述中知道测试的目的是什么。您有时会看到人们​​it​​根据测试的方法命名测试:

it('#filterProductsByQueryString', () => {})

但这无济于事——想象一下对这段代码很陌生,并试图弄清楚该函数究竟做了什么。在这种情况下,名称非常具有描述性,但如果你能想出一个真正的人类可读的字符串总是更好的。

命名测试的另一个指导方针是确保您可以阅读​​it​​开头带有的句子。因此,如果我正在阅读下面的测试,我会在脑海中读到一个大句子:

“它根据查询字符串过滤器过滤产品”

it('filters products based on the query-string filters', () => {})

不这样做的测试,即使字符串是描述性的,也会感觉很笨拙:

it('the query-string is used to filter products', () => {})

一个好的测试的三个部分

一旦你把你的测试命名好了,就该专注于身体了。一个好的测试每次都遵循相同的模式:

it('filters products based on the query-string filters', () => {
// STEP ONE: SETUP
// STEP TWO: INVOKE CODE
// STEP THREE: ASSERT
})

让我们依次完成每个步骤。

设置

任何单元测试的第一阶段都是设置:这是您按顺序获取测试数据的地方,或者模拟运行此测试可能需要的任何功能。

it('filters products based on the query-string filters', () => {
// STEP ONE: SETUP
const queryString = '?brand=Nike&size=M'


const products = [
{ brand: 'Nike', size: 'L', type: 'sweater' },
{ brand: 'Adidas', size: 'M', type: 'tracksuit' },
{ brand: 'Nike', size: 'M', type: 't-shirt' },
]


// STEP TWO: INVOKE CODE
// STEP THREE: ASSERT
})

该设置应建立执行测试所需的一切。在这种情况下,我将创建查询字符串和将用于测试的产品列表。请注意我对产品数据的选择:我有一些故意与查询字符串不匹配的项目,以及一个匹配的项目。如果我只有与查询字符串匹配的产品,则此测试无法证明过滤有效。

调用代码

这一步通常是最短的:你应该调用你需要测试的函数。您的测试数据将在第一步创建,因此此时您应该将变量传递给函数。

it('filters products based on the query-string filters', () => {
// STEP ONE: SETUP
const queryString = '?brand=Nike&size=M'


const products = [
{ brand: 'Nike', size: 'L', type: 'sweater' },
{ brand: 'Adidas', size: 'M', type: 'tracksuit' },
{ brand: 'Nike', size: 'M', type: 't-shirt' },
]


// STEP TWO: INVOKE CODE
const result = filterProductsByQueryString(products, queryString)


// STEP THREE: ASSERT
})

如果测试数据很短,我可能会合并第一步和第二步,但大多数时候我发现非常明确地将步骤拆分出来的价值值得它占用的额外行。

断言

这是最好的一步!这是您所有辛勤工作得到回报的地方,我们会检查我们期望发生的事情是否真的发生了。

当我们进行断言时,我将其称为断言步骤,但这些天我倾向于使用 Jest 及其​​expect​​功能,因此如果您愿意,您也可以将其称为“预期步骤”。

it('filters products based on the query-string filters', () => {
// STEP ONE: SETUP
const queryString = '?brand=Nike&size=M'


const products = [
{ brand: 'Nike', size: 'L', type: 'sweater' },
{ brand: 'Adidas', size: 'M', type: 'tracksuit' },
{ brand: 'Nike', size: 'M', type: 't-shirt' },
]


// STEP TWO: INVOKE CODE
const result = filterProductsByQueryString(products, queryString)


// STEP THREE: ASSERT
expect(result).toEqual([{ brand: 'Nike', size: 'M', type: 't-shirt' }])
})

有了这个,我们就有了一个完美的单元测试:

  1. 它有一个描述性的名称,读起来清晰且简洁。
  2. 它有一个明确的设置阶段,我们在其中构建测试数据。
  3. 调用步骤仅限于简单地使用我们的测试数据调用我们的函数。
  4. 我们的断言很清楚,并且清楚地展示了我们正在测试的行为。

小改进

虽然我实际上不会​​// STEP ONE: SETUP​​在我的真实测试中包含评论,但我确实发现在所有三个部分之间放置一个空行很有用。所以如果这个测试真的在我的代码库中,它看起来像这样:

it('filters products based on the query-string filters', () => {
const queryString = '?brand=Nike&size=M'
const products = [
{ brand: 'Nike', size: 'L', type: 'sweater' },
{ brand: 'Adidas', size: 'M', type: 'tracksuit' },
{ brand: 'Nike', size: 'M', type: 't-shirt' },
]


const result = filterProductsByQueryString(products, queryString)


expect(result).toEqual([{ brand: 'Nike', size: 'M', type: 't-shirt' }])
})

如果我们正在构建一个包含产品的系统,我希望创建一种更简单的方法来创建这些产品。我创建了​​test-data-bot​​库来做到这一点。我不会深入研究它是如何工作的,但它可以让您轻松地创建工厂来创建测试数据。如果我们有这样的设置(​​README​​有完整的说明),我们可以像这样进行这个测试:

it('filters products based on the query-string filters', () => {
const queryString = '?brand=Nike&size=M'
const productThatMatches = productFactory({ brand: 'Nike', size: 'M' })


const products = [
productFactory({ brand: 'Nike', size: 'L' }),
productFactory({ brand: 'Adidas', size: 'M' }),
productThatMatches,
]


const result = filterProductsByQueryString(products, queryString)


expect(result).toEqual([productThatMatches])
})

通过这样做,我们删除了与该测试无关的产品的所有细节(注意​​type​​现在我们的测试中不存在该字段),并让我们通过更新我们的工厂轻松保持我们的测试数据与真实数据同步。

我还将我想要匹配的产品拉到它自己的常量中,以便我们可以在断言步骤中重用它。这避免了重复并使测试更清晰 - 有一段测试数据标题​​productThatMatches​​强烈暗示这是我们期望我们的函数返回的内容。

结论

如果您在编写单元测试时牢记这些规则,我相信您会发现您的测试更易于使用,并且在您的开发工作流程中更有用。测试就像其他任何事情一样:它需要时间和练习。记住这三个步骤:​​setup​​​,​​invoke​​​,​​assert​​你会在不知不觉中编写完美的单元测试?。