关于前端测试的多方面介绍

139 阅读13分钟

我经常遇到前端开发者、经理和团队面临一个重复的、合理的难题:如何在单元、集成和E2E测试之间组织他们的测试,以及如何测试他们的UI组件。

单元测试似乎常常不能捕捉到发生在用户和系统上的 "有趣 "的事情,而E2E测试通常需要花费很长的时间来运行或需要混乱的配置。除此之外,还有很多工具(JEST、Cypress、Playwright等等)。如何理解这一切呢?

注意: 本文使用React作为例子和语义,但其中的一些价值适用于任何UI开发范式。

为什么测试前端是困难的?

我们不倾向于将前端作为一个系统来编写,而是作为一堆构成用户界面故事的组件和功能。由于组件代码主要存在于JavaScript或JSX中,而不是在HTML、JS和CSS之间进行分离,因此也比以往更容易将视图代码和业务逻辑代码混合起来。当我说 "我们 "时,我指的是我作为开发者或顾问遇到的几乎每一个网络项目。

当我们要测试这些代码时,我们常常从React测试库这样的东西开始,它可以渲染React组件并测试其结果,或者我们胡乱配置Cypress以使其与我们的项目很好地配合,很多时候最终会出现错误的配置或放弃。

当我们和经理谈论建立前端测试系统所需的时间时,他们和我们都不知道它到底需要什么,我们的努力是否会有结果,以及我们建立的东西对最终产品的质量和构建速度有何价值。

工具和流程

如果我们的团队有某种 "强制性TDD"(测试驱动开发)流程,或者更糟糕的是,有一个代码覆盖门,你必须有X%的代码被测试覆盖,情况就会变得更糟。作为一个前端开发员,我们完成了一天的工作,通过修复几行洒在几个React组件、自定义钩子和Redux还原器上的错误,然后我们需要想出一个 "TDD "测试来 "覆盖 "我们所做的事情。

当然,这不是TDD;在TDD中,我们会先写一个失败的测试。但是在我遇到的大多数前端系统中,没有基础设施来做这样的事情,而且在试图修复一个关键的bug时,要求先写一个失败的测试往往是不现实的。

覆盖率工具和强制性单元测试是我们行业迷恋特定工具和流程的症状。" 你们的测试策略是什么?"回答的方式往往是 "我们使用TDD和Cypress"或者 "我们用MSW模拟东西",或者 "我们用Jest和React测试库"。

一些拥有独立QA/测试组织的公司确实试图创建一些看起来更像测试计划的东西。然而,这些经常会遇到一个不同的问题,即很难与开发一起编写测试。

像Jest、CypressPlaywright这样的工具是很好的,代码覆盖率有它的位置,而且TDD是维护代码质量的一个重要实践。但是很多时候,它们取代了架构:一个好的接口计划,单元之间好的功能签名,一个清晰的系统API,和一个清晰的产品UI定义--一个好的关注点分离。流程不是架构。

糟糕的

为了尊重我们组织的流程,比如强制性测试规则或CI中的一些代码覆盖门,我们使用Jest或我们手头的任何工具,围绕我们所改变的代码库的部分模拟一切,并添加一个或多个 "单元 "测试来验证它现在给出了 "正确 "的结果。

除了测试难以编写之外,问题在于我们现在已经创建了一个事实上的契约。我们不仅要验证一个函数是否给出了一些预期的结果,而且还要验证这个函数是否具有测试所期望的签名,并以我们的模拟的方式使用环境。如果我们想重构这个函数的签名或它使用环境的方式,测试就会成为死物,一个我们不打算遵守的契约。它可能会失败,即使这个功能是有效的,也可能会成功,因为我们改变了内部的一些东西,模拟的环境不再与真实的环境匹配。

如果你正在写这样的测试,请停止。你在浪费时间,使你的产品的质量和速度变差。

最好不要有自动测试,而不是让测试在未指定的模拟环境中创造幻想世界,并依赖内部函数签名和内部环境状态。

合同

了解一个测试是好是坏的一个好方法是用简单的英语(或你的母语)写它的合同。契约不仅需要代表测试,还需要代表对环境的假设。例如,"鉴于用户名U和密码Y,这个登录函数应该返回OK"。一个合同通常是一个状态和一个期望。以上是一个很好的合同;期望和状态都很清楚。对于具有透明测试实践的公司来说,这不是新闻。

当合同被实施细节弄得一团糟时,情况就会变得更糟。"给定一个环境,这个useState钩子目前持有的值是14,Redux商店持有一个叫做userCache的数组,有三个用户,登录函数应该......"。

这个契约对于实现选择来说是非常具体的,这使得它非常脆弱。保持契约的稳定性,在有业务需求的时候改变它们,并让实现变得灵活。确保你从环境中依赖的东西是坚固的和定义良好的。

飘忽不定的人

当关注点分离缺失,我们的系统之间没有明确的API,我们缺乏有明确签名和期望的功能,我们最终用E2E作为测试功能或回归的唯一方法。这并不坏,因为E2E测试运行整个系统,并确保接近用户的特定故事按照预期运行。

E2E测试的问题在于其范围非常广泛。通过测试整个用户旅程,环境通常需要从头开始设置,通过认证,经历整个过程,找到新功能存在或回归发生的正确位置,然后运行测试案例。

由于E2E的性质,这些步骤中的每一步都可能产生不可预测的延迟,因为它依赖于许多系统,其中任何一个系统在CI运行时都可能出现故障或滞后,也依赖于对 "选择器"(如何以编程方式模仿用户正在做什么)的精心制作。一些较大的团队已经建立了根本原因分析的系统来做这件事,还有像testim.io这样的解决方案来解决这个问题。然而,这并不是一个容易解决的问题。

通常情况下,一个错误是在一个功能或系统中,而运行整个产品来达到这个目的的测试太多。新的代码修改可能会因为环境中的一些故障而在不相关的用户旅程路径中出现退步。

E2E测试在整个混合测试中肯定有它的位置,并且在发现非特定于子系统的问题方面很有价值。然而,过于依赖它们表明,也许不同系统之间的关注点和API障碍的分离定义得不够好。

好的

由于单元测试是有限的,或依赖于大量的模拟环境,而E2E测试往往是昂贵的和不稳定的,集成测试往往提供了一个很好的中间地带。通过UI集成测试,我们的整个系统在与其他系统隔离的情况下运行,这些系统可以被模拟,但系统本身的运行却没有修改。

当测试前端时,这意味着将整个前端作为一个系统运行,并模拟它所依赖的其他系统/"后端",以避免与你的系统无关的浮动和停机。

如果前端系统变得过于复杂,也要考虑将一些逻辑代码移植到子系统中,并为这些子系统定义一个清晰的API。

取得平衡

将代码分离成子系统并不总是正确的选择。如果你发现自己在每次改动时都要同时更新子系统和前端,那么这种分离可能会成为无益的开销。

当UI逻辑与子系统之间的契约可以使它们在某种程度上自治时,就把它们分离出来。这也是我对微观前端要小心的地方,因为它们有时是正确的方法,但它们专注于解决方案,而不是理解你的特定问题。

测试UI组件:分而治之

测试UI组件的困难是测试中一般困难的一个特例。UI组件的主要问题是它们的API和环境往往没有被正确定义。在React世界中,组件有一些依赖关系;有些是 "道具",有些是钩子(例如,上下文或Redux)。在React世界之外的组件经常依靠globals来代替,这是同一事物的不同版本。当看到常见的React组件代码时,如何测试的策略会让人困惑。

这其中有些是不可避免的,因为UI测试是很难的。但通过以下方式的划分,我们可以大大减少问题的发生。

将UI与逻辑分开

使测试组件代码更容易的主要原因是拥有更少的代码。看看你的组件代码,然后问,这部分是否真的需要以任何方式与文档相连?或者它是一个独立的单元/系统,可以被隔离测试?

你有越多的代码作为普通的JavaScript "逻辑",与框架无关,并且不知道它被UI使用,你就越少需要以混乱、不稳定或昂贵的方式来测试代码。另外,这些代码更容易移植,可以被移到Worker或服务器上,而且你的UI代码更容易跨框架移植,因为它的数量更少。

将UI构件与应用程序的小部件分开

让UI代码难以测试的另一件事是,组件之间有很大的不同。例如,你的应用程序可以有一个 "TheAppDashboard "组件,它包含了你的应用程序仪表盘的所有细节,还有一个 "DatePicker "组件,它是一个通用的可重复使用的部件,出现在整个应用程序的许多地方。

DatePicker是一个UI构件,它可以在多种情况下组成UI,但对环境的要求并不高。它不是针对你自己的应用程序的数据的。

另一方面,TheAppDashboard是一个应用程序的小部件。它在整个应用程序中可能不会被大量重复使用;也许它只出现一次。因此,它不需要很多参数,但它确实需要很多来自环境的信息,例如与应用程序的目的有关的数据。

测试UI构件

UI构件应该尽可能是参数化的(或在React中称为 "基于道具")。它们不应该从上下文(全局、Redux、useContext)中吸取太多东西,所以它们也不应该在每个组件的环境设置方面有很多要求。

测试参数化UI构件的一个明智的方法是设置一次环境(例如,一个浏览器,加上他们需要的其他环境),并在不重置环境的情况下运行多次测试。

这样做的项目的一个很好的例子是网络平台测试--由浏览器供应商用来测试互操作性的一套全面的测试。在许多情况下,浏览器和测试服务器被设置了一次,测试可以重新使用它们,而不是每次测试都要设置一个新环境。

测试应用小工具

应用小部件是上下文的,而不是 参数化的。 它们通常需要从环境中获得很多东西,并且需要在多个场景中操作,但使这些场景不同的通常是数据或用户交互中的东西。

我们很想用测试UI构件的方式来测试应用部件:为它们创造一些假的环境,以满足所有不同的钩子,然后看看它们产生了什么。然而,这些环境往往是脆弱的,随着应用程序的发展而不断变化,这些测试最终会变得陈旧,并对小工具应该做什么给出不准确的看法。

测试上下文组件的最可靠的方法是在它们的真实环境中--用户看到的应用程序。用UI集成测试和有时用e2e测试来测试这些应用部件,但不要费力地通过模拟UI或utils的其他部分来对它们进行单元测试。

可测试的用户界面小贴士

摘要

前端测试很复杂,因为通常UI代码缺乏关注点的分离。业务逻辑状态机与框架特定的视图代码纠缠在一起,上下文感知的应用程序部件与孤立的、参数化的UI构建块纠缠在一起。当一切都纠缠在一起时,唯一可靠的测试方式就是在一个不稳定且成本高昂的e2e测试中测试 "一切"。

为了管理这个问题,要依靠架构而不是具体的流程和工具:

  • 将你的一些业务逻辑流转换成与视图无关的代码(例如,状态机)。
  • 将构件与应用部件分开,以不同的方式测试它们。
  • 模拟你的后端和子系统,而不是前端的其他部分。
  • 对你的系统签名和合同进行反复思考。
  • 尊重你的测试代码。它是你的代码的一个重要部分,而不是事后的考虑。

在前端和子系统之间以及不同的策略之间取得适当的平衡是一种软件架构技术。要做到这一点是很难的,需要经验。获得这种经验的最好方法是通过尝试和学习。我希望这篇文章能对学习有一些帮助!