一种深思熟虑的测试自动化方法
作者/译者:Arek Frankowski/ 溜的一比
来源网站名称:Medium
来源文章网址:https://medium.com/@globus1987/beyond-scripting-a-thoughtful-approach-to-test-automation-400afdbfcc85
测试窝链接:
在软件开发项目中,我们经常会遇到与代码质量相关的挑战。虽然大多数团队将重点放在生产代码上,但重要的是认识到测试自动化代码也值得同样的关注。Kim Filiatrault 在一篇有见地的文章中强调了生产代码与测试代码之间经常被忽视的区别。在Testμ大会上,Paul Grizzaffi 强调我们应将测试自动化视为软件,并优先考虑其质量。测试代码质量不仅仅是代码评审和使用 SonarCloud 等工具进行静态分析,它还包括整个架构设计。
在与 SGI 等 Guidewire 客户合作并使用 CenterTest 框架时,我们遵循了一些特定的规则,以确保高质量、可维护的测试自动化代码。
在这篇文章中,我们将探讨组织测试自动化代码的四步过程。在每一步中,我们都会重点介绍我们的方法如何自然而然地遵循面向对象设计的 SOLID 原则:
- 单一职责原则(SRP)
- 开闭原则(OCP)
- 里氏替换原则(LSP)
- 接口隔离原则(ISP)
- 依赖倒置原则(DIP)
我们将讨论我们的设计和架构如何符合 SOLID 原则,从而消除传统脚本化的方式,转而使用高度可维护、可扩展的可重用类结构,这为 CenterTest 中的 InsuranceSuite 测试自动化提供了完整的基础,并带来了以下几个关键优势:
- 提高效率:通过消除重复脚本,减少了创建和维护测试所需的时间和精力。
- 增强可扩展性:我们的可重用类结构允许随着应用程序的增长轻松扩展测试覆盖范围。
- 提高可靠性:组织良好、模块化的代码更不容易出错且更易于调试。
- 更好的协作:清晰、结构化的方法使团队成员更容易理解和贡献测试套件。
- 更快的入职:新团队成员能够快速掌握并使用设计良好的测试自动化框架。
- 长期成本节约:虽然初期设置可能需要更多时间,但维护和扩展的便利性会在项目生命周期中带来显著的时间和资源节约。
这种综合方法不仅提高了测试自动化的质量,还使其更紧密地与软件工程的最佳实践相结合,最终为更强大、更可靠的 InsuranceSuite 实现奠定了基础。
理解 CenterTest 和页面对象模型(POMs)
在深入探讨我们的组织过程之前,有必要理解我们测试框架的一个关键方面。使用 CenterTest,我们可以直接从 Guidewire 源代码生成页面对象模型(POM)类。这些 POM 类仅包含页面元素,提供了页面结构与其行为的清晰分离。
值得注意的是,这些 POM 类应专注于访问元素,我强烈建议避免将 POM 直接与功能性代码连接。这种分离遵循了 SOLID 的单一职责原则(SRP),确保每个类只有一个变更的理由。
前期步骤:单体测试
让我们看一个自动化工作通常开始的典型例子。这更像是一个脚本而不是正式的测试代码——它涵盖了使用 Guidewire PolicyCenter 创建保单所需的所有步骤,但没有真正的设计,仅仅是操作流程的简单流。虽然它使用了 CenterTest,但缺乏合适的结构,也没有应用面向对象原则。
这是很多自动化工作开始的样子:功能上可行,但没有考虑长期的可维护性或可扩展性。下面的例子展示了我们将在接下来的步骤中解决的挑战:
示例单体脚本
这个单体测试虽然功能齐全,但在可读性、可维护性和可重用性方面存在几个挑战。我们只展示了开头部分,因为整个脚本约有 600 行代码,涵盖了 Guidewire PolicyCenter 的完整流程。现在,我们将拆解这个测试,并对其进行重新组织,以提高效率和清晰度。
第一步:将单体测试分解为方法
我们的组织过程的第一步是将单体分解为更小、更易管理的方法,遵循 SRP。
最初,我们的测试可能包含账户创建、保单创建和保单验证在同一个方法中。而保单创建本身是一个复杂的向导过程,我们需要经过政策信息、驾驶员、车辆、保险范围、报价和支付等步骤。通过将整个测试分解为独立的方法,例如 createAccount()
、setPolicyInfo()
、setDrivers()
、setVehicle()
、setCoverages()
、quote()
、processPayment()
和 verifyPolicyDetails()
,我们引入了模块化的层次结构,带来了多个好处,并符合 SRP。
这种划分提高了可读性,每个方法都有一个清晰的、单一的目的,使其他开发人员更容易理解测试的流程。此外,它还增强了可维护性。如果账户创建过程发生变化,例如,您只需更新 createAccount()
方法,而不会影响测试的其余部分。
这些较小的方法促进了重用性,因为它们可以很容易地从其他测试用例中调用,减少代码重复,并确保测试套件的一致性。
步骤1.将代码分离为方法
这种重构为随后的更复杂的组织奠定了基础,并确保每个方法都有单一且明确的职责。
第二步:将方法组织为可重用类
随着测试套件的扩展,我们会发现这些较小、专注的方法数量不断增加。虽然这相对于我们最初的单体测试是一个显著的改进,但在管理许多方法时也带来了新的挑战。这时我们采取第二步——将这些方法组织为可重用类,牢记开闭原则(OCP)。
关键在于将相关功能分组在一起,同时确保类可以扩展而不需要修改。例如,我们可以将所有与账户创建相关的方法移入 CreateAccount
类,而将处理保单的方法放入 CreatePolicy
类中。
这种组织不仅使代码更加逻辑化,还显著提高了其可重用性和可扩展性。此外,我们根据所处理的行为使用命名约定,这使我们可以使用 CenterTest Narrative——框架提供的 BDD 报告。
让我们来看一下重构后的结果。
CreateAccount 类检查账户是否存在,并在必要时创建该账户
当前的测试状态
这种结构允许我们在不修改现有类的情况下轻松扩展功能,符合 OCP。如果我们需要添加新的账户类型或保单操作,我们可以扩展这些基础类,而不是直接修改它们。
第三步:创建扩展类
接下来,我们可能会遇到一些场景,在这些场景中我们需要在现有类的基础上构建特定功能。这时继承的优势就体现出来了,并遵循了里氏替换原则(LSP)。通过创建扩展类,我们可以为特定目的添加或更改功能,而无需更改我们的基础类,确保子类的对象可以与父类对象互换使用。
让我们考虑一个场景,我们需要创建一个业务账户,该账户需要比个人账户更多的信息。为此,我们可以扩展 CreateAccount
类来处理这一需求,并使用 CreateBusinessAccount
:
类扩展以处理新类型的账户
现在我们可以创建一个使用这些特殊类的测试:
使用新类测试
这种方法允许我们处理特殊场景,同时保持基础类的结构和优势。尤其是,它促进了代码重用,并保持测试代码的 DRY(Don't Repeat Yourself)原则。确保在需要时可以使用扩展类而不影响基本类的使用,符合 LSP。
虽然扩展类提供了很大的灵活性,但必须谨慎使用它们。过度使用继承可能会导致复杂的类层次结构,难以维护。始终考虑组合是否比继承更适合您的具体使用场景。
例如,与其创建一个继承自 CreatePolicy
的 CreateBackdatedPolicy
类,不如通过简单的数据条件处理它。这种方法通常提供了更大的灵活性,并避免了里氏替换原则的潜在问题。
第四步:为可重用类创建工厂
随着我们的测试套件变得更加复杂,涉及不同类型的账户和保单,管理这些对象的创建会变得具有挑战性。这时工厂模式变得非常有用,我们可以利用接口隔离原则(ISP)和依赖倒置原则(DIP)。通过实现工厂类,我们可以将测试对象的创建集中化,使得管理依赖关系和配置更加容易。
让我们来创建个测试工厂类
现在,我们可以重构我们的测试以使用这个工厂:
这种工厂实现看似简单,但由于我们底层的类结构,它很好地遵循了 SOLID 原则:
CenterTest中的类层次结构
这种结构意味着虽然我们的工厂方法返回的是具体类,但这些类是抽象层次结构的一部分。CenterTest 客户端与工厂创建的对象交互时,实际上使用的是 Scenario
接口或 BaseScenarioPC
抽象类。
这种方法通过依赖抽象(Scenario
接口和抽象类)而不是依赖具体实现,符合 DIP。它还通过提供与我们的 Scenario
接口和抽象类中隔离的职责相一致的工厂方法,支持 ISP。
使用 TestFactory 方法有以下几个优点:
- 对象创建集中化:所有测试对象都在一个地方创建,简化了依赖关系和配置的管理。
- 维护性提高:如果需要更改对象的创建或初始化方式,只需更新工厂类。
- 灵活性:可以轻松切换不同的实现或添加新类型的对象,而不影响现有测试。
- 减少重复:工厂消除了在多个测试类中重复实例化对象的需求。
在实现工厂模式时,您可以选择使用简单工厂进行简单对象创建,或在需要创建一系列相关对象时使用抽象工厂。在我们的案例中,我们将使用抽象工厂来创建不同类型的账户(个人、业务)及其关联的保单。
工厂的扩展可能性
这种方法不仅集中化了对象的创建,还使得未来添加新类型的账户或保单变得更加容易,支持了开闭原则。
通过实施这四个步骤——拆分单体测试(SRP)、将方法组织成可扩展的类(OCP)、创建可替换的扩展类(LSP)、使用工厂和隔离接口(ISP 和 DIP)——我们大大改进了测试自动化代码的结构和可维护性,同时符合 SOLID 原则。
从重构到前期设计
随着您对这些原则和实践的熟悉,您会发现您的测试自动化方法在不断演变。从一开始的重构现有代码的过程,逐渐转变为从零设计新测试的方法。
随着您在现有测试套件中应用这些步骤,您将更加直观地从一开始就以这种模块化、可维护的方式构建新测试:
- 自然而然地考虑具有单一职责的小型、专注的方法。
- 本能地将相关功能组织成内聚的类。
- 预见潜在的变化,并从一开始就为扩展性设计。
- 主动地实现工厂和其他设计模式,而不是被动地使用。
这种从被动重构到主动设计的转变是您通向高质量测试自动化旅程中的一个重要里程碑。这意味着您不仅在解决问题,还在防止问题的发生。此外,您的测试套件从一开始就变得更加健壮、灵活和可维护。
请记住,目标是持续改进。当您内化这些原则时,您将发现自己从一开始就在编写更简洁、更高效的代码。在长期来看,这种主动的方法节省了时间,减少了技术债务,并为整个团队带来了更愉快、更高效的测试过程。
为 Guidewire 测试设计
现代集成开发环境(IDEs)是改进 Guidewire InsuranceSuite 测试自动化的强大助手。这些工具使得重构变得轻而易举——您可以通过几次点击重命名方法、提取类并重新组织代码。这意味着您可以轻松地增强现有测试,而不需要彻底重写。
对于 Guidewire 测试,您并不是从头开始。CenterTest 为您提供了一套全面的可重用、可扩展的类,涵盖了开箱即用的产品线和流程。这意味着您不需要从头搭建基础设施——它已经存在,可以直接使用。
CenterTest 中的这些预构建类涵盖了保险操作、保单管理以及 Guidewire 应用程序中的常见和复杂的工作流程。它们的设计易于定制,使您可以根据具体的实现进行调整,而无需重新发明轮子。
例如,如果您需要测试带有特定保险范围的个人汽车保单创建,您不需要编写所有基本的保单处理代码。CenterTest 已经提供了保单管理的类。您只需使用这些类,并专注于扩展它们以处理客户保险范围的自定义逻辑。
目标不是在 CenterTest 之上构建,而是利用它提供的内容,并在需要的地方进行自定义。这种方法使您可以专注于您独特的测试需求,而不是设置基础设施。通过利用 CenterTest 的现成组件并应用良好的设计原则,您可以在数小时内为您的 Guidewire 实现创建一个既全面又可维护的测试套件。
结论
在我们结束对测试自动化代码组织的探讨时,让我们花点时间反思一下我们所讨论内容的实际影响。
我们从一个常见的问题开始:难以阅读、维护和扩展的单体测试用例。通过将这些拆分为较小、专注的方法,再将它们组织成可重用类,我们创建了一个在日常工作中更容易处理的结构。想象一下,当登录流程发生变化时,节省了多少时间——现在您只需要修改一个类中的一个方法,而不必更新几十个测试用例。
引入扩展类为我们提供了处理特殊情况的灵活性,而不会混乱我们的基础类。这意味着我们可以包含不同类型的账户或保单,而不需要重写现有测试或使我们的核心类复杂化。
我们的 TestFactory 方法看似多了一层,但考虑一下它简化了测试编写。新成员可以快速上手,只需几个简单的方法调用就可以创建测试对象,而不必理解对象初始化的复杂细节。
这些改进不仅仅是理论上的——它们转化为实际的好处:
- 更快的 bug 修复:当测试失败时,容易找到问题所在,因为每个方法都有一个明确的单一目的。
- 更快的测试开发:拥有一套可重用的组件,新测试的创建更多的是在现有部分的基础上进行拼装,而不是从头开始编写。
- 更容易维护:当被测试系统发生变化时,我们的模块化方法意味着我们很可能只更新测试代码的一小部分。
- 更好的协作:团队成员可以处理测试套件的不同部分,而不会互相干扰,因为我们清楚地分离了关注点。
当您回到您的项目中时,可以考虑从小处开始。也许先从拆分一个大测试用例开始,或者为常见操作(如登录)创建一个可重用的类。您不需要在一夜之间重构整个测试套件。相反,随着编写新测试和更新现有测试时,逐步应用这些原则。
记住,目标不是追求完美,而是持续改进。每一次朝着更好地组织测试代码迈进,都是朝着更可维护、更可靠的测试套件迈出的一步。而这意味着更好的软件、更快乐的团队和更满意的最终用户。
所以,带着这些想法,根据您的具体需求进行调整,并开始构建一个不仅检查您的软件,而且有助于推动其质量向前发展的测试自动化框架。您的未来自己(和您的团队)会感谢您。