写点什么

自动化测试的分层结构

2009 年 10 月 26 日

摘要

在测试自动化中,测试代码中不仅仅包含测试逻辑,还包含许多其他代码,比如 URL 拼接、html/xml 解析、访问 UI 控件,等等。若把测试逻辑与这些无关代码混在一起,测试逻辑将会很难理解, 也不容易维护。本文会介绍如何用分层结构来解决测试自动化中遇到的这些问题。在这个分层结构中,测试自动化代码会被分成三层:(1)测试用例层,表达应用程序的测试逻辑。(2)领域层, 用业务领域术语来给待测系统建模,封装 HTTP 请求、浏览器控制、结果解析逻辑等,给测试用例层提供一个接口。(3)待测系统层,第 2 层构建在这一层之上。

问题

QA 的工作包括设计测试用例、探索性测试(exploratory testing)及回归测试,等等。这些工作有的依靠 QA 的聪明才智, 而有的却只是重复劳动(例如回归测试)。随着系统中不断地加入新功能,回归测试这类工作耗费的时间也越来越多。

测试自动化可以解决这个问题。测试自动化后,重复性的劳动会由计算机来做,而测试用例都用计算机程序来表述, 因此 QA 可以从重复劳动中解脱出来,有更多的时间用在创造性的工作上来。

在测试自动化中,测试代码并不仅仅包含测试逻辑,也包含许多其他的支撑代码,例如 URL 拼接、HTML/XML 解析、UI 控件访问等。 例如要测试一个能接受不同搜索参数,并返回包含特定信息的 XML(例如用户数据)的 web 服务,测试代码需要:

  1. 根据待测操作拼接 URL
  2. 使用 HTTP 库发起 HTTP 请求
  3. 读取 web 服务器返回的信息,并解析数据
  4. 对比返回的数据与期望数据

有的测试自动化代码,会把 URL 拼接、HTML/XML 解析、XPath 表达式,和测试逻辑写在一起,通常在同一个类或方法中。

这种方法很容易入手,且很直观,因为其反映了测试人员手工测试的过程。但是这种方法存在一定的问题:

  1. 测试逻辑难以理解及修改。当测试逻辑与一大堆无关代码混在一起时,很难辨别出测试逻辑。 要添加新测试用例,通常需要重读这些支撑代码才能找到需要修改的代码。测试逻辑也会很难理解。
  2. 测试变得很脆弱。因为测试逻辑和 html 解析等支撑代码混在一起,待测系统和自动化测试直接的‘契约’若稍有变化, 自动化测试将无法运行。例如,若 UI 发生变化,比如把 input 元素挪到另一个 div 元素下, 或者改变某个 UI 元素的 ID,所有相关的测试自动化代码都会受到影响。
  3. 维护开销大。一组完备的测试用例会对系统的某个部分进行多组测试,而每组测试间都会存在重复的代码。例如这些代码可能都要 (1)根据待测操作拼装 URL,(2)发出 HTTP 请求,(3)解析 web 服务器返回的信息,(4)比较实际结果及期望结果。因为在各个测试用例间存在 重复代码,如果这个过程发生任何改变,则需要修改各个测试用例的代码。

解决方法

软件开发领域曾遇到过同样的问题,并找到了解决方法,即‘层次结构’(Layered Architecture)。引用《领域驱动设计–软件核心复杂性应对之道》(‘Domain-driven design: tackling complexity in the heart of software’)一书:

“分层结构的价值在于每一层只关注于程序的特定方面。这使得每个方面的设计都很紧凑,也更容易理解。当然,使用层次结构的最重要原因是把各个重要的方面都分隔开。“

虽然测试自动化领域关注的是测试领域,但是所遇到的问题的本质却是一样的,因此可以应用相似的解决方案:

测试用例层 这一层包含所有(并只有)测试逻辑。有了下一层即领域层帮忙,测试逻辑可以很清晰、简洁地表达出来。不同用户故事、场景及边界条件 都构建领域层之上,区别只在于测试数据。 领域层 这一层封装了对待测系统的所有操作,例如 URL 拼接、XML 或 HTML 解析,富客户端或浏览器的控制,等等。通过这一层包装, 待测系统可以以业务领域语言的形式供调用者使用,而非以 xpath、sql 或者 html 等技术“语言”形式。这层的目的在于提高抽象层次。 测试的目的是验证业务逻辑是否实现地正确。若测试能用业务领域的语言编写,那么测试目的就一目了然了。 待测系统层 即要测试的系统

测试用例层包含许多测试用例。这些测试用例都是基于领域层的。领域层用领域语言封装了待测系统。
领域层直接访问待测系统。

例子

假设我们要测试一个 restful web 服务。通过这个 web 服务,我们可以用电话作为关键字搜索客户信息。

要调用这个 web 服务,需要发起以下格式的 HTTP 请求:

复制代码
http://{endpoint}/subscribers?telephoneNumber={telephoneNumber}

服务端返回的以竖线分割的数据包含客户的姓名、电话、地址及其他信息:

复制代码
13120205504|ST|C|SQ|112|||FIRST|ST|W|Riverfront|BC|010|68930432|

测试这个服务的用例为:(1)用能精确匹配一个用户的电话作为关键字搜索,(2)用能精确匹配多个用户的电话作为关键字搜索,(3)用 不完整电话作为关键字搜索等。用例的完整程度完全取决于 QA 的想象能力。

对于每个测试用例,执行的数据基本上都一样:(1)拼装包含电话号码关键字的 URL,(2)用 HTTP 库发出 HTTP GET 请求,(3)解析数据, (4)把真实值与期望值做比较。为了避免上面提到的问题,我们在这里采用分层结构:

测试用例层

这一层的具体实现方式与采用的测试框架有关。在这个例子中,我们采用 C#及 NBehave

复制代码
[Story]
public class SearchCustomerbyTelephoneNumberStory: TestBase
{
[Scenario]
public void SearchWithAPhoneNumberWhichHasAnExactMatch()
{
story.WithScenario("Search with a phone number which has a exact match")
.Given(AN_ACCOUNT_WITH_PHONE_NUMBER, "01068930432", EMPTY_ACTION)
.When(SEARCH_WITH, "01068930432",
SEARCH_WITH_ACTION)
.Then(ACCOUNT_INFORMATION_SHOULD_BE_RETURNED, "13120205504",
ACCOUNT_INFORMATION_SHOULD_BE_RETURNED_ACTION)
.Given(AN_ACCOUNT_WITH_PHONE_NUMBER, "01062736745")
.When(SEARCH_WITH, "01062736745")
.Then(ACCOUNT_INFORMATION_SHOULD_BE_RETURNED, "12666056628");
}
[Scenario]
public void SearchWithPartialPhoneNumber()
{
story.WithScenario("Search with partial phone number")
.Given(THREE_ACCOUNTS_WITH_PHONE_NUMBER_STARTS_WITH, "0106", EMPTY_ACTION)
.When(SEARCH_WITH, "0106", SEARCH_WITH_ACTION)
.Then(ACCOUNT_INFORMATION_SHOULD_BE_RETURNED, "13120205504",
ACCOUNT_INFORMATION_SHOULD_BE_RETURNED_ACTION)
.And(ACCOUNT_INFORMATION_SHOULD_BE_RETURNED, "12666056628")
.And(ACCOUNT_INFORMATION_SHOULD_BE_RETURNED, "17948552843");
}
[Scenario]
public void SearchWithAPhoneNumberWhichHasSeveralExactMatches() {...}
[Scenario]
public void SearchWithNonExistentPhoneNumbers() {...}
[Scenario]
public void SearchWithInvalidPhoneNumberValues() {...}
...
...
}

这些测试用例用 C#写成,但是很接近英语,即使非技术人员也可以读懂。 (请参照 Martin Fowler 的 BusinessReadableDSL )。这样,其他的团队成员,特别是对领域更熟悉的业务人员,可以很容易的读懂测试用例, 因此也更可能指出测试中遗漏的案例及场景。

若采用支持以自然语言形式书写测试用例的框架(例如Ruby 平台下的 Cucumber )则会更好。

以"ACTION"结尾的变量为 lambda 表达式。他们是真正的测试逻辑。

SEARCH_WITH_ACTION 会向 web 服务发出请求,并会解析返回的以竖线分割的数据。类 CustomerService 和 Subscriber 在领域层中,他们 会在多个测试中重复使用。

复制代码
SEARCH_WITH_ACTION =
phoneNumber =>
{
subscribers = customerService.SearchWithTelephoneNumber(phoneNumber);
};

ACCOUNT_INFORMATION_SHOULD_BE_RETURNED_ACTION is for verifying the data

复制代码
ACCOUNT_INFORMATION_SHOULD_BE_RETURNED_ACTION =
accountNumber =>
{
//Get expected subscriber from fixture
Subscriber expected = SubscriberFixture.Get(accountNumber);
CustomAssert.Contains(expected, subscribers);
};

领域层

CustomerService 类以真实 web 服务的名称命名。在需求文档、日常对话、架构图以及代码中,都用这个名称来指代此 web 服务。 使用统一的名称,能除去二义,提高沟通效率。

复制代码
public class CustomerService
{
public Subscriber SearchWithTelephoneNumber(string telephoneNumber)
{
string url =
string.Format(
"{0}/subscribers?telephoneNumber={1}",
endpoint, telephoneNumber);
//Send http request to web service, parse the xml returned,
//populate the subscriber object and etc.
return GetResponse(url);
}
...
}

Subscriber 类建模了用户。比起用竖线分割的字符串,增加一层数据抽象,用对象表示返回的数据,能使 测试更容易理解(你应该不会偏好用 pipedData[101] 表示电话号码吧?)。

复制代码
public class Subscriber
{
public string AccountNumber { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public string TelephoneNumber { get; set; }
...
}

有了这些领域模型,测试就能直接构建在这些对象上了。例如,可以如此验证所返回的用户名为’Bei’:

复制代码
Assert.AreEqual("Bei", subscriber.FirstName);

或者电话号码以’010’开始:

复制代码
Assert.IsTrue(subscriber.TelephoneNumber.StartsWith("010"));

点击这里可以下载到样例代码。代码中演示了如何用分层架构组织测试自动化代码。 你可以在 Visual Studio 2008 中打开项目,也可以在命令行运行执行‘go.bat’来运行所有测试。 ‘go.bat’运行完后会将测试结果保存在‘artifacts’文件夹。源代码中包含三个项目。 名称以 with ‘Client’的项目包含领域层。以‘Client.Spec’结尾的项目为领域层对应的 单元测试(TDD)。‘Stories’项目包含测试用例层。这份源代码由真实项目中来,并作了相应修改。某些 类返回了硬编码的值,是为了不访问真实的 web 服务。

这如何能解决问题?

  1. 问题:‘测试逻辑难以理解和修改’。现在我们有了一个单独的层表示测试逻辑。这层构建在领域层之上,因此测试可以 很用简洁、紧凑的自然语言形式表述,因此阅读、理解、推理和修改测试用例的难度,更取决于编码人员的语言能力,而非编码水平。
  2. 问题:‘测试很脆弱’。因为我们有一个单独的层把测试用例和待测系统隔离开,若待测系统有任何变化,只有此层 会受到影响。只要在此层做相应修改,构建于此层之上的测试用例仍然可以执行。
  3. 问题: ‘维护开销大’。因为有了领域层的封装,各个测试用例中不会再有重复代码。要做修改,也只需修改一处。此外, 因为领域模型直接针对待测系统建模,代码也跟容易理解和修改。

常见问题解答

问题:这个方法看起来有些复杂,必须要这么做吗?

回答:这主要取决于待测系统的规模和复杂程度。如果系统规模较小、业务逻辑相对简单,这个方法就过于笨重了。在这种情况下, 甚至连测试自动化都可能是浪费时间。如果只花几分钟时间就能手动测试整个系统,那还自动化干什么呢?若系统较为复杂, 把测试逻辑和支持代码混合在一起问题应该不大。而对业务逻辑复杂、规模庞大的系统(也就是说,大部分企业级应用) 我偏好这种方式。

问题:若采用这种结构,那么在开始‘真正’的测试前,需要投入一定时间搭建整个结构,会不会很浪费时间?

回答:这只是另外一种组织代码的方式。即使代码不按照这种方式组织,还是要写代码拼装 URL、解析 XML / HTML、验证测试结果。 采用这种结构,只需要把代码拆分到不同的类及方法中。此外,没有必要一次完成整个结构。可以根据当前的测试需要,逐步完成整个结构。

问题:完成这个结构需要相当的面向对象知识,并不是所有 QA 都可以做。

回答:实际上测试自动化并不只是 QA 的职责。项目中其他成员,包括开发人员,也可以参与。

开发人员有很强的编程功底,编写出的代码质量也相对较高,因此可以负责领域层。而 QA 擅长设计测试用例、找出各种边界测试条件,因此可以 负责测试用例层。

作者简介:李贝,ThoughtWorks 的咨询师,主要兴趣在于领域驱动设计、测试自动化及领域专属语言。本文原文《 Layered Architecture for Test Automation 》于 2009 年 8 月 11 日发表在 InfoQ 英文站。

相关阅读

[ ThoughtWorks 实践集锦(1)] 我和敏捷团队的五个约定

[ ThoughtWorks 实践集锦(2)] 如何在敏捷开发中做好数据迁移

[ ThoughtWorks 实践集锦(3)] RichClient/RIA 原则与实践(上)(下)

[ ThoughtWorks 实践集锦(4)] 为什么我们要放弃Subversion

[ ThoughtWorks 实践集锦(5)] “持续集成”也需要重构

[ ThoughtWorks 实践集锦(6)] Mock 不是测试的银弹

[ ThoughtWorks 实践集锦(7)] 环境无关的环境

[ ThoughtWorks 实践集锦(8)] Tech Lead 的三重人格


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2009 年 10 月 26 日 00:028031

评论

发布
暂无评论
发现更多内容

程序员什么时候就该辞职了?

Java架构师迁哥

从实际案例聊聊Java应用的GC优化

AI乔治

Java 编程 架构 JVM GC

TronChain波场链合约系统开发技术

薇電13242772558

区块链 智能合约

架构师训练营 -week06-作业

大刘

极客大学架构师训练营

英特尔第十一代处理器 (代号Rocket Lake-S) 架构详情

intel001

K近邻算法:机器学习萌新必学算法

华为云开发者社区

学习 算法

GitHub上最励志的计算机自学教程(重制版),前端小白到亚马逊工程师

沉默王二

GitHub 学习 程序员 面试

创新方案百花齐放,英特尔助力2020 EdgeX中国挑战赛推动智能边缘行业创新及人才发展

intel001

使用 Maven Archetype 基于 IDEA 快速创建项目

程序员小航

Java maven 开发 项目 Archetype

LeetCode题解:78. 子集,递归+for循环+回溯,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

Java-技术专题-Object克隆方法解析

李浩宇/Alex

Apache Doris在京东广告的应用实践

DorisDB

数据库 大数据 数据仓库

区块链钱包开发app,去中心多币种钱包搭建

WX13823153201

区块链钱包开发

架构师训练营第六周作业

Shunyi

极客大学架构师训练营

【得物技术】一文读懂Vue生命周期

得物技术

Vue 生命周期 得物技术部 得物 钩子函数

JVM 源码解读之 CMS GC 触发条件

AI乔治

Java 架构 JVM GC

手撕面试题:多个线程顺序执行问题

海星

Java 面试 多线程

第二周作业

小兵

蚂蚁金服首发887页Java面试宝典!还原真实面试情景+面试题

Java架构追梦

Java 编程 架构 面试 蚂蚁金服

Java-技术专题-LocalDate和LocalTime和LocalDateTime

李浩宇/Alex

架构师训练营 - 第 6、7、8、9、10 、11、12、13周学习总结(1 期)

阿甘

JavaScript 对象 — 重学 JavaScript

三钻

Java 前端 对象

京东推荐系统中的兴趣拓展如何驱动业务持续增长?

京东智联云开发者

算法 推荐系统 知识图谱

对抗验证概述

计算机与AI

学习 数据验证

Java-技术专题-volatile关键字

李浩宇/Alex

直播带货需要运营者实名验证:规范行业有利于健康发展

石头IT视角

Javassist实现JDK动态代理

AI乔治

Java 编程 架构 jdk

第二周总结

小兵

Redis可以做哪些事?

Java旅途

redis

用上ConcurrentHashMap,就没有并发问题了?

海拉鲁

Java 并发

Week 6 命题作业

阿泰

自动化测试的分层结构-InfoQ