测试对我和我的团队来说是一个无价的工具,我认为它们是我多年来建立起来的工程工具带中的一个关键部分。在Wayfair上,团队使用测试来维护代码质量,作为测试驱动的开发工具,并作为自记录新特性的一种方式。当您拥有一组可信任的测试,即“测试套件”时,所有这些实用程序都是可能的;通过产生一致的结果来工作的测试套件。

在我们的测试套件中大约有10,000个测试,我们发现自己处于噩梦般的场景中。我们有过一些不可靠的测试,这些测试给了我们假阳性或阴性的结果,但没有明显的错误。我们会使一个试验失效,另一个试验就会失败——到处都有误导的地方。在最坏的情况下,我们的工程师甚至会偶尔阻止部署代码,因为幻影测试失败。我们的测试套件不再可信。

这篇文章是关于我们如何识别这些流氓测试中,我们是如何解决他们,以及我们如何去约防止它们再次发生。我们保证我们的测试套件可以信任和依靠,也许你会发现你怎么能对自己的测试做同样的。

一点历史

Wayfair犯了一个反应的大转变在2017年年初由于反应过来,我们的工程师开始将更多的时间花在显著发展我们的应用程序时编写JavaScript。这带来了对JavaScript的测试重点,因为我们希望确保我们的JavaScript应用起到预期,无缺陷。新的框架的特征,共享的组件库,特征代码 - 所有的它需要进行测试。我们决定,我们需要更新我们的老化测试茉莉花亚军跟上我们的团队的测试需求。我们转身开玩笑因为它是自然的继承者茉莉花。多亏了它们几乎相同的api,我们能够相当快地进行转换。

引入新技术是一回事,而确保您的团队正确使用这些技术则是另一组问题!事实证明,在新技术和测试方法上增加1000多名工程师并不是一件简单的任务。我们需要大量的帮助来确保我们拥有合适的测试模式和工具,因此我们在过去几年做了很多来确保这一点。我们运行(现在仍然在做)培训项目,讨论测试和JavaScript。我们也提供自助式培训,名为“真棒学习”该工程师可以在自己的节奏一组设置解决。

除了教育之外,我们还将改进的工具整合到我们的流程中。为工程师提供实时反馈的工具。例如,使用ESLint,则eslint-插件,笑话帮助我们避免常见的问题,在测试之前,他们曾经使它到代码审查。我们的静态分析不限于ESLint - 我们也有相当多的搭载SonarQube流量类型的JavaScript和日常静态分析工作。所有这一切都是为了确保我们有一个健康的代码库测试所覆盖。

在编写本文时,我们的通用测试套件已经增长到超过14000个测试,另外还有1000多个分散在我们不同的内部框架、服务和库中的测试。随着测试数量的增加,测试本身的问题也在增加。尽管我们做了所有的努力,但我们还没有做足够的工作来确保测试本身没有缺陷!

测试中隐藏的挑战

事后看来,这可能是显而易见的,但测试就是代码。代码是由工程师编写的,而工程师就是会犯错的人,没有人能免于这个规则。错误会导致错误,而测试代码本身的错误会导致(如果幸运的话)不稳定的测试。是的,如果你有一个不稳定的测试,你是幸运的,因为这被认为是一个数据点,一个提示你某些事情不正确的信号。

我们的优先分配片状测试的过程是直线前进(尽管具体的解决方案可能不是)。首先,隔离是通过在测试套件跳过它产生不一致的结果的测试,然后开出罚单到创作团队,要求他们修补试验。最后,团队返回并产生一个固定的工作替代其合并到测试套件。

在更棘手的测试,以修复是一个提供假阳性结果,其中一个给了你这件事被打破任何数据。一个测试悄悄离开嗡嗡声,总是传球。这个测试甚至可能去尽可能打破一些其他的测试,完全无关系吧 - 有什么好玩的。这种类型的测试是最阴险的,但也是最有趣的修复!让我们看看一些例子,是比较通用的 - 而在实时的测试可能要复杂得多,这些例子足以凸显的问题。

不应该传递

不幸的是,识别测试反模式的一个大障碍被Jest本身复杂化了。在Jest中测试是否失败的逻辑很简单:如果它抛出错误,则失败,如果没有,则通过。

基于这个逻辑,这里是一个很好的经验法则:不要让测试断言是有条件的。如果测试中的条件有0.01%的可能是假的,那么就假设它是假的。永远让你的断言是无条件的。顺便说一下,这并不局限于if语句,还有大量隐含的条件语句,在编写测试时可能并不明显。以这个测试片段为例:

试验( '函数添加4',()=> {常量原始= [1,2,3,4]; const的结果= FN(原件); results.forEach((结果,ⅰ)=>期望(结果)。砥([i]原+ 4));});

你可以在这里发现的问题?该fn(原始)调用返回一个空数组?你猜对了,这个测试将通过。是的,测试未能断言什么,但它也没有抛出任何错误,所以它仍然通过就好了。棉短绒不要么抓住这个,但有人肯定应该写一皮棉它。这是阵营的UI测试,其中一名工程师试图断言的东西上呈现元素的集合的共同问题,他们可能不会在所有的渲染,没有断言实际发生。然而,测试不会失败。

异步汤

这是条件问题的变化,但现在的条件期望是异步逻辑,如预期或许没有运行的结果。看一下这个:

test('promise are tough', () => {let promise = promise .resolve();const wrapper =  wrapper.find(…).模拟('click');promise.then(() = >期待(…).toBe(…));//断言一些东西返回承诺;});

如果您已经编写了足够的笑话测试并使用了足够的承诺,那么问题就很明显了。该那就是()期望()呼应该是return语句的一部分。但是,这是否检验合格?是的,是它。事情变得真正有趣这里,静态棉短绒没有赶上这种情况下,它运行时的测试通过。你也可能会认为这是很容易修复通过添加expect.hasAssertions ()通过确保对断言进行解释,达到测试的顶端。这也不会在测试中失败,至少在编写时是这样。断言计数随着Jest中expect调用的增加而增加,并且这些数字是异步收集的,因此具有异步断言的非异步测试仍然通过各种方式。有趣的是,如果断言失败,测试将不会失败。下面有更多关于这方面的内容。

在严格模式开玩笑测试

静态地捕捉上面的问题是相当困难的(例如用linters)。在一个足够复杂的测试套件中,这些反模式有许多排列,检查时它们并不是那么明显的错误,这使得很难跟踪它们。相反,您需要的是自动化运行时检查和保护措施,以防止这种逻辑渗透到您的测试中。这就是为什么在Wayfair我们为我们的笑话测试开发了一种我们称之为“严格模式”的东西。它作为一种运行时保护措施来指导我们的工程师,防止编写糟糕的测试进入我们的测试套件。

我们的“严格模式”修补程序(后面会更多),该API玩笑的某些部分,并确保两个基本运行时检查是在每个测试执行的执行:

  • 没有断言,任何测试都失败
  • 在测试后,测试防止任务调度已经完成

前者多为LINTED,但也有难以捕捉的情况下运行时检查是必要的。后者是一个非常重要的支票,必须在运行时进行,以确保没有任何一个测试是“泄漏”般划过测试边界的承诺或超时任务。您还需要后者,以确保以前的作品如预期开玩笑。你可以在这个链接的GitHub问题中了解更多。

它是如何工作的?

该引擎,权力我们玩笑“严格模式”是一个小玩笑插件称为“玩笑 - 插件,必须断言”,这是可用在这里。所有你需要做的是安装的软件包和插件的玩笑配置添加到“setupFilesAfterEnv”(对于玩笑24+)的列表,就像这样:

{setupFilesAfterEnv:[ “开玩笑-插件-必断言”]}

一旦你这样做,有两件事情开始发生。首先,如果没有断言任何测试将开始失败。其次,任何试图测试计划任务后,试验本身是完整的将被阻止,并警告将被打印到控制台:

console.warn SRC / index.js:72测试“未归还承诺断言”试图测试完成之后调用microTask(Promise.then)。见堆栈跟踪了解详情。onInvokeTaskDefault(SRC / index.js:14:11)Object.onInvokeTask(SRC / index.js:81:22)____________________Elapsed_1_ms__At__Wed_Jul_17_2019_12_57_10_GMT_0400__Eastern_Daylight_Time_(HTTP://本地主机)然后(E2E /失败/ __测试__ / index.js:23:21)回调(SRC / index.js:112:59)

我们发现这些堆栈跟踪要追查到底是什么一段代码试图触发差事任务回调是必不可少的。这些痕迹是在帮助我们追踪的警告做出反应上卸载组件的setState电话和不合时宜的网络请求无价的。

该插件本身做一些复杂的机动,以“打补丁”的玩笑API,以使每一个测试被包裹在自己的。这是由优秀的启用Zone.js图书馆由角队赞助。什么是区域?

区域是跨异步任务持久存在的执行上下文。你可以把它想成线程本地存储针对JavaScript虚拟机“。

区域库为strict mode插件提供了跟踪异步事件(如超时和承诺)的能力,这在其他情况下是不可能的。我可以单独写一篇关于区域以及为什么它们对这项工作如此重要的博客文章,但那得改天再说。我鼓励你在上面的GitHub链接中阅读更多关于它的内容。

您的里程可能有所不同

当我们第一次尝试在我们的测试套件“严格模式”玩笑配置结果并不漂亮。我相信,我们发现我们的测试是无论是片状的3%左右的东西(总〜10K测试),无意遗失声明,或提供假阳性结果。虽然比例可能较低,这造成十分严重的套件的其余部分的破坏可能是大得不成比例。这可能不是每个人的情况下,虽然。一个是有利于我们工作的事情是,我们可以推出分阶段这种严格的模式配置,并允许团队逐渐提交补丁对他们的测试。一个单独的配置也使工程师可以在本地运行的测试,严格的版本,这是我们作为后一个要求启用不久。到现在为止,我们已经推出了严格的模式,我们所有的CI的工作,因为它在默认情况下烤到我们所有的测试配置。

当你读过,严格的模式已经相当对我们很有帮助,但你的里程可能有所不同。您可能需要考虑一些额外的策略来避免这些问题。

避免人为错误

虽然这是很普通的通知,这是所有你可能需要的一些球队。一个较小的团队,所有的开发商都在同一页上,同意测试理念和大家评论对方的代码。根据我的经验,两个以上的工程师的任何一支球队将受益于作为测试尽可能多的自动化保障。出于好意的工程师犯错误,甚至在我们测试的大多数代码库,我们的框架的区域,我们发现的假阳性测试非零量。

首先确保您的测试失败

如果你是测试驱动开发的实践者,你会认识到这一点宗旨。我发现,谁是在TDD过程中无私的工程师还扔掉的智慧,这块金块与它一起。即使你写测试的事实后的代码,不遵循严格的TDD,你还是应该确保测试可以失败。如果不验证失败情况,你不会真正知道肯定测试工作按预期。

在测试中避免间接

间接是对立的一个很好的测试案例。避免过多的帮手和间接的测试,你就不太可能有不确定的测试结束。出现断测试的非平凡量,因为工程师“B”误读如何使用工程师“A”用于一些测试辅助。例如,缺少一个承诺或丢失断言的关键回调。一吨的间接使得它非常具有挑战性的事实,这使得他们更难以修复,如果出现了问题后,破译测试的意图。

写小测试

这是确保你的测试不是片状,并最终有用的一般,审判和真正的方法。一个令人费解的测试的一个明确的信号是一个与“和” S之名,“应该插件渲染和获取数据... AND发射导弹”。该测试越大,越变量这取决于这意味着更多的东西,可以和会出问题。撰写更有针对性的测试,其负责在同一时间测试系统的特定功能,并非所有的功能!

考虑其他测试运行程序

其中一个主要的原因写一个完整的插件玩笑是缺少用于没有断言而失败测试的配置选项。由于写这篇文章的时候,似乎不太可能被添加到玩笑的未来。如果你开始了和正在检查可用的测试运行,你可能要考虑的出色avajs。Avajs对于如何编写异步测试(异步测试是bug的主要来源)要严格得多,并且由于在核心API中缺少全局变量,Avajs内置了不需要断言就能让测试失败的能力。

结论

今天,我们的测试套件哼着远,搭载了玩笑。这些问题是不容易识别或修复。幸运的是,灵活的玩笑API保释我们出去,使我们能够通过一个插件补丁额外的保障到我们的测试套件。

如果您对jest-plug - in-must-assert感兴趣,并希望做出额外的贡献,或者有更好的选择,请在GitHub库。如果您有任何其他意见,并想直接与我联系,你可以联系我的Twitter@ballercat。给我你的盛大测试获胜!您还可以看看我谈谈我们的测试磨难,在今年的2019年波士顿反应下面:

我们一直在寻找更多对前端和测试感兴趣的工程师——如果这可以描述的话,你会毫不犹豫地去寻找申请一个我们的开放前端职位。