全栈测试:平衡单元测试和端到端测试

全栈开发人员的特点是能够从头到尾交付并发布一个特性。教程和书籍常常侧重于搭建全栈开发环境和让测试能够进行所需要的“管件(plumbing)”(我综合运用了Angular、Rails、Bootstrap和Postgres)。但对于如何贯穿整个Web开发栈进行应用程序测试,却常常缺少指导。让我们深入研究下这篇文章。我们将学习如何充分利用端到端测试,包括对测试什么以及如何保证那些测试的可靠性和可维护性进行指导。我们还将谈及单元测试以及它们在端到端测试策略中的作用。但首先,我们要理解编写测试的根本目的。\

从根本上讲,测试是为了确保应用程序的行为符合开发者的意愿。它们是自动化的脚本,执行代码并检查其行为是否符合预期。测试编写得越好,就越可以依赖它们为部署把关。如果测试不充分,就需要一个QA团队或者发布有缺陷的软件(两者均意味着用户获得价值的速度比理想情况慢许多)。如果测试充分,就可以自信而快速地发布,不需要批准或者像QA那样缓慢的人工过程。\

对于编写的测试,还必须权衡未来的可维护性。应用程序会变,因此测试也会变。在理想情况下,测试的修改与软件的修改是成正比的。如果你修改了一条错误信息,那么你不会希望大量重写测试套件。但是,如果你彻底地修改了一个用户流程,那么可以预料,将有大量的测试需要重写。\

实际上,这意味着你无法将所有测试都作为端到端的全面集成测试,但是你也不能只进行少得可怜的单元测试。这就关乎如何达成那种平衡。\

测试的类型

\

测试的种类很多,但对于本文而言,我们就谈论两类:端到端测试和单元测试。\

端到端测试模拟用户行为。在Web应用程序中,他们会启动服务器,打开浏览器,到处点击,断言浏览器中发生了特定的事情,让我们相信功能可以正常运行。这些测试会给我们巨大的信心,但是它们缓慢而脆弱,并且同用户界面紧密地耦合在了一起。\

单元测试根据代码单元的公共API运行它们。这些测试需要创建一个类的实例,使用特定的输入调用它的方法,断言被调用的方法达到了预期的效果(通常是返回了预期的输出)。这些测试快速而稳定,并且不会同系统的其他部分紧密地耦合在一起。不过,它们无法让你相信整个系统可以正常运行——只是测试过的代码单元可以正常运行。\

构建一项特性的任务就是要在两类测试之间找到恰当的平衡点。如果端到端测试太多,那么未来修改应用程序就会痛苦而缓慢。如果太少,那么一些不易觉察的缺陷就会进入到生产环境,即使快速测试套件的代码覆盖率为100%。\

从用户体验入手

\

你的软件是向某个用户提供服务,因此,那个用户应该推动你的工作。我不建议使用测试来设计用户体验,因此,要在编写测试之前弄清楚用户将如何使用软件(要么通过试验性代码,要么同一名设计师一起工作)。一旦弄清楚了,就可以开始工作了。\

在理想情况下,你将为用户体验的某个部分创建端到端的测试,并编写代码让其通过测试。在编写那些代码的时候,你会创建单元测试,具体化需要创建或修改(通常是后者)的代码的规范。\

问题是,编写没有用户界面工件(HTML)可供参考的、端到端的失败测试很难。这是因为,大部分端到端测试的形式都是:\

  1. 找到页面上的某个元素; \
  2. 通过某种方式同它交互; \
  3. 证实交互成功; \
  4. 重复上述过程直到测试结束。

这意味着,围绕要发生交互的用户界面元素(DOM对象),你需要有一些规范。当把以JavaScript为基础的交互设计考虑在内时,如果不实际地构建界面,至少是部分地构建,就更难测试了。\

为此,要让一个粗略的UI轮廓在浏览器中运行起来。使用预先准备好的数据,并且不需要考虑备选流程——一次专注于一件事。它运行起来以后,就可以编写测试了。\

在这样做的时候,有两点需要考虑:这个特性需要测试吗?如果需要,该如何测试?\

测试什么

\

虽然在编程上没有愉快路径,但用户经历的代码路径要比代码的可能路径少许多。例如,当用户购买一款产品,根据用户地址、选择的发货方式或者以前的购买历史,我们可能会用不同的方式处理订单。在所有情况下,用户的体验都是一样的,这样,在用户看来,流程只有一个。\

这时,你的目标是测试所有的用户流程。你需要一个测试套件,模拟一个用户做你想要并希望他做的事,并断言你想要提供给该用户的所有体验都工作正常。\

假如你已经知道要测试什么,那应该如何进行呢?\

如何进行端到端测试

\

如果修改了一个流程,那么就要修改那个流程的测试。由于端到端测试模拟用户活动,所以不需要为想要断言的每件事情都编写一个测试。如果用户应该在结算界面上看到三段重要的信息,就不需要编写三个测试——一个测试检查所有三段信息就足够了。因此,当修改一个现有的用户体验时,要找一个现有的、可以改进的测试。\

否则,就需要一个新的测试。记住,你的目标是模拟用户要做的事情。务必要对如何组织测试中的导航和行为开诚布公。用户真地会直接导航到某些深层链接吗?或者他们会点击某个公用的开始页面从而到达他们需要到达的地方吗?\

这很难做,尤其是通常要使用最少的标记实现该功能。测试需要定位特定的DOM元素同其交互,而准确找到你想要同其交互的元素并不总是很简单(或者可能)。你需要“标识(signpost)”。\

标识是专门插入DOM中用于定位感兴趣的元素的。要尽早确定这些标识如何发挥作用。不应该使用原本用于样式化的CSS类来定位DOM元素。这样做意味着前端开发人员改变类名就会破坏测试。也不应该使用被JavaScript代码使用的CSS类或数据属性(比如前缀为js-的类)。这会带来同样的破坏。\

使用前缀为test-的CSS类或者前缀为data-test-的属性是两种常用的技术:

\u0026lt;section class=\"component dark test-checkout-confirmation\"\u0026gt;\  \u0026lt;!-- ... --\u0026gt;\\u0026lt;/section\u0026gt;\\\u0026lt;!-- 或者 --\u0026gt;\\\u0026lt;section class=\"component dark\" data-test-checkout-confirmation\u0026gt;\  \u0026lt;!-- ... --\u0026gt;\\u0026lt;/section\u0026gt;\
\

这可能看上去让人不舒服……也确实是。但是,与将测试耦合到内容或者展示类相比,这就不那么令人讨厌了。这里,你需要寻求一种平衡——不要盲目地使用data-test属性标记每个元素。例如,如果你想点击一个购买特定产品的按钮,那么你真正需要的只是定位某个包含那款产品及购买按钮的元素。

\u0026lt;article data-test-product=\"1234\"\u0026gt;\  \u0026lt;!-- a ton of markup --\u0026gt;\  \u0026lt;input type=\"submit\" name=\"Purchase\" value=\"Purchase\"\u0026gt;\\u0026lt;/article\u0026gt;\\u0026lt;article data-test-product=\"5678\"\u0026gt;\  \u0026lt;!-- a ton of markup --\u0026gt;\  \u0026lt;input type=\"submit\" name=\"Purchase\" value=\"Purchase\"\u0026gt;\\u0026lt;/article\u0026gt;\
\

添加data-test-product属性后,你就能够使用一个像[data-test-product='1234'] input[type='submit']这样的CSS选择器定位产品1234的购买按钮了。\

这意味着你必须修改只为测试而存在的标记,就是说,为了获得你提供给他们的用户体验,用户要下载一些他们不需要的字节。这是一种平衡,但比糟糕的测试覆盖率(对用户的伤害远远超过了HTML中多一些额外的字节)要好。只是得恰到好处。\

当页面上有改变页面内容而又不重新加载的交互(换句话说,使用JavaScript)时,这项技术就更加重要了。\

处理交互

\

当每次点击都重新加载页面时,端到端测试更可靠,因为底层工具知道要等待一个页面重新加载。当用户交互只是改变DOM时,难度就大了,因为工具不知道什么“事情”正在发生,也就无法“等待事情完成”。\

当测试需要同一个不会根据用户动作重新加载的页面交互时,就需要一种方法能够在开始断言发生了什么之前等待DOM操作完成。如果不等待,那么如果测试开始断言时DOM还没有更新,测试就会无谓地失败。\

就像在标记中使用标识定位要操作的DOM元素一样,我们也可以把它们用在这里。任何新增或变化的标记都应该有某种在交互失败或没有发生的情况下不会出现的标识。换句话说,你不必为了等待DOM事件而在测试中进行休眠调用——DOM中应该包含可供测试显式等待的标识。\

例如,假设我们想要测试一个动作为用户生成了一条成功的消息。假设实现方法是发出一个AJAX请求,当调用结束时向DOM中插入一条消息。一个基本的实现可以像下面这样做:

function purchase(productId) {\  $.post(\        \"/products/\

你可能感兴趣的:(测试,javascript,ui,ViewUI)