[译] 完美的 Javascript 单元测试

今天咱们讨论的是如何编写完美的单元测试以及如何确保测试的可读性,可用性和可维护性。javascript

我发现有一个共性,那些告诉我单元测试没用的人,一般都在编写糟糕的单元测试。特别对于那些刚刚接触单元测试的新手,这彻底能够理解。写出很棒的单元测试很难,它须要你常常练习才能够。咱们今天要讨论的全部事情都是经过很艰难的方式学到的; 不良单元测试的痛苦导致我为如何编写好的单元测试建立了本身的规则。咱们今天要讨论的就是这些规则。前端

为何糟糕的测试很是致命?

若是你拿到的程序代码很混乱,那么就会很难处理。但万幸的是你能够为你的代码写一些单元测试,它们能够帮到你。若是能有测试支持你,那么处理那种混乱的代码还 OK。测试能够帮助你消除不良代码的影响。java

可是不会有任何代码能够帮你处理一个糟糕的测试。你不能为你的测试再去编写测试。你也能够,可是接下来你就必须为你的测试的测试编写测试,一个无穷无尽的循环,没有人想要这样……android

不良测试的特色

很难定义一组不良测试的特征,由于不良测试不符合咱们即将要讨论的任何规则。ios

若是你曾经看过一个测试而且不知道它正在测试什么,或者你没法明显地识别它的断言,那就是一个糟糕的测试。若是一个测试的描述写的很差(我最喜欢用 it('works')),那它就是一个糟糕的测试。git

若是你发现测试没有用,那么它也是一个糟糕的测试。测试的所有目的是提升你的生产力、工做流程和对代码库的信心。若是测试作不到这些(或者让它变得更糟),那么它确定是一个糟糕的测试。github

我坚信糟糕的测试比没有测试更糟糕后端

好的测试都有一个好名字

好消息是,一旦你习惯了测试,那些好的测试规则很容易记住,并且很是直观!bash

一个好的测试有一个简洁、描述性的名称。若是你不能想出一个简短的名字,那么最好选择清晰明确的名称而不是仅仅省下了每行的长度。函数

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

你应该可以从描述中了解到测试的目的是什么。你有时会看到下面这种写法,基于要测试的方法名称给 it 测试命名:

it('#filterProductsByQueryString', () => {})
复制代码

但这并无帮助 —— 想象一下若是你刚刚接触这些代码,你还得费力找出这个函数到底有什么功能。在这种状况下,方法名称是很是具备描述性的,可是一个真正人类可读的字符串老是更好,前提是你能想出来一个。

为测试命名的另外一个指导方针是:确保你能够在 it 开头读取到句子。因此,若是我正在阅读下面的测试,我会读到一句话:

“it filters products based on the query-string filters(它基于查询字符串过滤器过滤产品)”

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', () => {
  // 第一步:初始化
  // 第二步:调用代码
  // 第三步:断言
})
复制代码

让咱们依次看看这些步骤。

初始化

任何单元测试的第一个阶段都是初始化:这是你按顺序获取测试数据的地方,也是模拟运行此测试可能须要的任何功能的地方。

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' },
  ]

  // 第二步:调用代码
  // 第三步:断言
})
复制代码

初始化这步应该构建执行测试所需的一切。在上面的例子中,我建立了查询字符串和我将用于测试的产品列表。注意我为产品列表挑选的测试数据:我有一些故意与查询字符串不匹配的数据,以及一个彻底匹配的数据。若是我只有与查询字符串匹配的数据,那么这个测试不能证实过滤是有效的。

调用代码

这步一般是最短的:你应该在这里调用须要测试的函数。第一步中你应该已经构造了测试数据,因此在这里你能够直接将它们做为变量传递给函数。

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)

  // 第三步:断言
})
复制代码

若是测试数据很是少,我可能会合并第一步和第二步,但大部分时间我都发现将它们明确地按步骤拆分很是有价值,值得多写几行。

断言

这是最好的一步!是你全部的努力获得回报的地方,咱们在这里检查事情有没有按照咱们指望的进行。

我称之为断言步骤,由于咱们在这里作一些断言,可是如今我倾向于使用 Jest 和它的 expect 函数,因此若是你愿意的话你也能够称之为“指望步骤”。

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' }])
})
复制代码

通过上面这些操做,如今咱们有了一个完美的单元测试:

  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 库,它能够轻松作到上面的事情。我不会深刻介绍它的工做原理,但它可让你轻松地建立**工厂模式(factories)**来构建测试数据。若是咱们用了这个构建工具(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 的测试数据自己就是一个强烈的暗示,告诉咱们这就是指望函数返回的内容。

总结

若是你在编写测试的时候遵循了上面的规则,我相信你必定会发现你的测试更容易使用,并且对你的开发流程更有帮助。测试和其它任何事情同样:须要时间和练习。记住三个步骤:setupinvokeassert,你必定能够写出完美的单元测试😼。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索