如何编写更好的测试 - 在单元测试前必须回答的5个问题

大多数开发人员不知道如何测试

每个开发人员都知道我们应该编写单元测试,以防止将缺陷部署到生产中。

大多数开发人员不知道的是每个单元测试的基本要素。我无法开始计算我看到单元测试失败的次数,完全不知道开发人员试图测试什么功能,更不用说它出了什么问题或为什么重要。

在我最近的一个项目中,我们让一大片单元测试进入测试套件,但完全没有描述测试的目的。我们有一个很棒的团队,所以我放松了。结果?只有作者才能真正理解的大量单元测试。

幸运的是,我们完全重新设计了API,我们将把整个套件扔掉并从头开始 - 这将是我修复列表中的优先级#1

不要让这件事发生在你身上。

为什么进行测试?

您的测试是防范软件缺陷的第一道防线。您的测试比linting和静态分析更重要(它只能找到错误的子类,而不是实际程序逻辑的问题)。测试与实现本身一样重要(重要的是代码满足要求 - 如果实现得不好,它的实现方式根本不重要)。

单元测试结合了许多功能,使它们成为应用程序成功的秘密武器:

  1. 设计辅助:首先编写测试可以让您更清晰地了解理想的API设计。
  2. 功能文档(面向开发人员):测试描述在代码中包含每个实现的功能要求。
  3. 测试开发人员的理解:开发人员是否足够理解问题,以便在代码中阐明所有关键组件的要求?
  4. 质量保证:手动QA容易出错。根据我的经验,开发人员在更改重构,添加新功能或删除功能后,无法记住需要测试的所有功能。
  5. 持续交付援助:自动化质量保证提供了自动防止破坏的构建部署到生产的机会。

单元测试不需要扭曲或操纵来满足所有这些广泛的目标。相反,单元测试的基本性质是满足所有这些需求。这些好处都是良好编写的具有良好覆盖率的测试套件的副作用。

TDD(测试驱动开发

有证据说:

  • TDD可以降低错误密度。
  • TDD可以鼓励更多模块化设计(提高软件灵活性/团队速度)。
  • TDD可以降低代码复杂性。

科学说:大量的经验证据表明TDD有效**。

先写测试

Microsoft Research,IBM和Springer tested 优先测试与后测试方法的效果,并始终发现优先测试处理比后来添加测试产生更好的结果。它非常明确:在您实施之前,请编写测试。

在实施之前,写测试。

什么是良好的单元测试?

好的,所以TDD有效。首先编写测试。要更有纪律。相信这个过程......我们明白了。但是你怎么写一个好的单元测试?

我们将从一个真实的项目中看一个非常简单的例子来探索这个过程:来自Stamp Specificationcompose()函数。

我们将use tape进行测试,因为它有着透明度和简洁性。

在我们回答如何编写良好的单元测试之前,首先我们必须了解如何使用单元测试:

  • 设计辅助:在设计阶段,在实施之前编写。
  • 功能文档和开发人员理解测试测试应提供正在测试的功能清晰描述。
  • 质量保证/持续交付:测试应在故障时停止交付管道,并在失败时生成错误报告。

单元测试作为Bug报告

当测试失败时,该测试失败报告通常是您关于确切出错的第一个也是最好的线索 - 快速追踪根本原因的秘诀就是知道从哪里开始寻找。当您有一个非常明确的错误报告时,这个过程会变得更加容易。

失败的测试应该读起来像高质量的bug报告。

好的测试失败bug报告中包含什么?

  1. 你在测试什么?
  2. 它应该做什么?
  3. 输出是什么(实际)?
  4. 预期输出(预期行为)是什么??

良好故障报告的示例


首先回答“你在测试什么?”:

  • 您正在测试哪些组件方面
  • 该功能应该做什么?您正在测试哪些特定的行为要求?

The compose() function takes any number of stamps (composable factory functions) and produces a new stamp.

compose()函数应该测试的内容,并产生相应的结果。

要编写此测试,我们将从任何单个测试的最终目标向后工作:测试特定的行为要求。为了使这个测试通过,代码必须产生什么特定的行为?

该功能应该做什么?

我喜欢从写一个字符串开始。没有分配给任何东西。没有传递给任何函数。只需明确关注组件必须满足的特定要求。在这种情况下,我们将从compose()函数返回一个函数开始。

一个简单,可测试的要求:

'compose()应该返回一个函数。'

现在我们将跳过一些内容并充实其余的测试。这个字符串是我们的目标。事先说明它有助于我们关注奖品。

我们测试的组件方面是什么?

“组件方面”的含义因测试而异,具体取决于为测试组件提供足够覆盖所需的粒度。

在这种情况下,我们将测试compose()函数的返回类型,以确保它返回正确类型的东西,而不是undefined或什么都没有,因为它在你运行它时抛出。

让我们把这个问题转换成测试代码。答案出现在测试描述中。这一步也是我们调用函数并传递回调函数的地方,当测试运行时,测试运行器将调用这个回调函数:

test('<What component aspect are we testing?>', 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 = '<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';

你可以构建一个断言,而不是专门为名为actualexpected变量赋值但我最近开始专门为每个测试中的actualexpected 变量赋值,发现它使我的测试更容易读。

看看它如何澄清断言?

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();
});

下次编写测试时,请记住回答所有问题:

  1. 你在测试什么?
  2. 它该怎么办?
  3. 什么是实际的输出?
  4. 什么是预期的输出?
  5. 如何复制测试?

最后一个问题由用于派生“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();
});

使用单元测试还有很多,但知道如何编写一个好的测试还有很长的路要走。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容