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

发表于:2016-7-26 11:57

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:InfoQ    来源:51Testing软件测试网采编

  处理交互
  当每次点击都重新加载页面时,端到端测试更可靠,因为底层工具知道要等待一个页面重新加载。当用户交互只是改变DOM时,难度就大了,因为工具不知道什么“事情”正在发生,也就无法“等待事情完成”。
  当测试需要同一个不会根据用户动作重新加载的页面交互时,就需要一种方法能够在开始断言发生了什么之前等待DOM操作完成。如果不等待,那么如果测试开始断言时DOM还没有更新,测试就会无谓地失败。
  就像在标记中使用标识定位要操作的DOM元素一样,我们也可以把它们用在这里。任何新增或变化的标记都应该有某种在交互失败或没有发生的情况下不会出现的标识。换句话说,你不必为了等待DOM事件而在测试中进行休眠调用——DOM中应该包含可供测试显式等待的标识。
  例如,假设我们想要测试一个动作为用户生成了一条成功的消息。假设实现方法是发出一个AJAX请求,当调用结束时向DOM中插入一条消息。一个基本的实现可以像下面这样做:
function purchase(productId) {
$.post(
"/products/",
{ "id": productId }
).done(function() {
$(".header").html(
"<div class='alert-success'>Your order was placed</div>");
}).fail(function() {
$(".header").html(
"<div class='alert-failure'>There was a problem</div>");
});
  你可以通过配置让测试等待一个使用了CSS类alert-success的元素出现,然后断言它的内容。这意味着,如果页面需要任何其他使用那个类的元素,那么测试就会不可靠或被破坏。虽然你可以将其限制在HTML头里,但这只是缓兵之计。
  作为替代,可以使用data-test-属性:
function purchase(productId) {
$.post(
"/products/",
{ "id": productId }
).done(function() {
$(".header").html(
"<div data-test-purchase-successful class='alert-success'>Your order was placed</div>");
}).fail(function() {
$(".header").html(
"<div data-test-purchase-failed class='alert-failure'>There was a problem</div>");
});
  虽然这增加了标记的字节,但它让你可以编写一个能够不受某些视觉变化影响的可靠测试。只要页面流程是在一次成功的购买后显示一条消息,那么可视化实现就可以修改而又不破坏测试。这是你想要的,这是一种权衡。你也可以牺牲掉这份自信,创建最小最起码的标记,但当显示效果变化时,你要么花时间修复测试,被迫手动QA,要么就发布没有经过充分测试的软件。
  如今的端到端测试工具,如Capybara,包含你需要的所有功能。它提供了方法,可以在继续测试过程之前等待DOM元素出现,断言页面特定部分的内容,同表单元素交互。大多数其他Web应用程序栈都提供了类似的工具。不管怎样,你可以将测试库与像PhantomJS这样的无界面浏览器结合,从而使端到端测试出奇地快速可靠。
  还有一点值得注意,就是在一个分布式的环境中如何完成这项工作。
  当“应用”多于一个
  当对单个整体系统进行测试时,上述技术就完全够用了。然而,如果是对一个较为分散的系统进行测试,情况就要复杂些了。假设你正致力于一个面向客户的应用程序,但它必须从另一个系统获取库存数据。你如何为此编写一个测试呢?
  首先,记住你在测试什么。端到端测试是测试用户交互。这意味着,端到端测试不用负责断言远程服务的功能,也不用负责断言应用程序正确地消费了那个远程服务。
  测试服务消费的最佳方式是使用“消费者驱动的契约(consumer-driven contracts)”,这是一种单元测试的形式(至少在这篇博文中我所做的宽泛界定中是这样)。
  对于在端到端测试中如何模拟远程服务,至此仍然没有定论。你可以搭建该服务的一个实际版本,但这并不是很好。你最终不得不管理那个服务的内部数据存储以及它所依赖的服务。那会使复杂性迅速增加,难以管理。
  一个常见的选择是使用一个HTTP层的模拟系统。在Ruby中,VCR是一款具备这种功能的工具。你录制同真实服务交互以建立HTTP协议往返的过程,在随后运行测试时,模拟系统会回放录制好的交互,而不必使用网络。如果单元测试覆盖了服务的正确消费,那么这对于端到端测试就会很有效。
  另一个选择是搭建一个经过简化的模拟服务,该服务返回预先准备好的数据。应用会像平常一样进行HTTP调用,但调用的是一个预先准备好、只向应用返回静态已知数据的服务。这需要提前做些配置,但对简单的服务交互很有效。如果应用程序需要在服务中存储状态,并有一个漫长的往返“对话”,那么这项技术就要难一些了。
  我的建议是首先尝试模拟HTTP,因为那既简单又快捷。
  现在,我们知道在端到端测试中测试什么以及如何测试,那么单元测试呢?
  单元测试
  回想一下,对于什么应该进行端到端的测试,我们的标准是用户流程。其思想是,虽然整个系统有许多可能的逻辑流程,但能对用户体验产生影响的要少很多。单元测试就是要测试那些逻辑流程的剩余部分。
  这让我们可以快速可靠地断言系统大部分功能的正确行为。换句话说,虽然我们可以使用端到端测试断言整个系统中每个可能的流程,但那没有必要,而且会非常缓慢和脆弱。
  例如,假设一个结算功能有两个用户流程:一个是购买成功,一个是购买失败,用户必须重试。那会有两个端到端测试。让我们进一步假设,后台有如下可能性:
  客户的信用卡正确扣款;
  与客户银行的通信存在问题,但我们想假装它是成功的,并在稍后扣款;
  客户的信用卡被拒绝;
  客户的信用卡过期。
  这是四个流程,所以我们希望有四个单元测试可以断言其中每一种情况都得到了正确处理。是的,会有重复覆盖。在端到端测试中,我们可能会创建成功扣款和拒绝两个测试来处理该功能的两个用户流程,因此,当编写单元测试时,我们的覆盖率就会超过理论上的需要。
  再一次,这是一种权衡,但重要的是,单元测试可以很好地覆盖你的类。这就允许它们改变位置、用途,而且更容易修改。
  关于如何编写单元测试,有许多许多的理论,远远超出了我们这里的讨论范围。我的建议是采用一种对你有用同时也容易跟别人解释的技术,并一直使用。
  对于单元测试,最困难的部分是决定代码设计要在多大程度上为测试考虑。这就类似我们如何为了测试向HTML中增加属性和其他标识——那些工件只是因为我们要测试而存在。在编写单元测试时,你会面临同样的选择。
  例如,假设Purchaser类实现了信用卡扣款代码。假设它将使用第三方提供的AwesomePayments进行实际地扣款。
class Purchaser
def charge(purchase)
AwesomePayments.charge(purchase.customer.id,purchase.amount)
rescue => ex
try_again_later(purchase.id)
end
# ...
end
  上述代码清晰易懂,在不需要单元测试的情况下,这可能是最理想的设计了。然而,为了让测试更简单,我们可能想控制AwesomePayments的实例:
class Purchaser
def initialize(awesome_payments = AwesomePayments)
@awesome_payments = awesome_payments
end
def charge(purchase)
@awesome_payments.charge(purchase.customer.id,purchase.amount)
rescue => ex
try_again_later(purchase.id)
end
end
  现在,就可以在测试时传入AwesomePayments的模拟实现,从而更好地控制测试。测试已经影响了我们的设计(虽然这里的影响比较小)。你甚至可以说,这个类就是更好的代码。但情况并非总是如此。
  我会使用同你处理端到端测试一样的标准:做让生活更轻松的事,但不要做过头,务必要恰到好处。
  小结
  从头到尾实现一个特性的能力取决于从头到尾测试它的能力。由QA团队或客户测试代码的反馈循环存在极大的危害。即使有QA团队,他们也不应该找到Bug,如果要想快速发布软件,就不会介意编写用户行为的端到端测试。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号