写点什么

自动化测试的分层结构

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:028223

评论

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

2020年书单

井中人

生产环境全链路压测建设历程17:某快递A股上市公司的生产压测案例之前言

数列科技杨德华

全链路压测 七日更

小白干货奇遇记

熊斌

个人成长 七日更

执法办案信息化建设,情报研判管控分析平台搭建解决方案

t13823115967

智慧公安

做音视频最好用的几款跨平台框架

anyRTC开发者

flutter uni-app ios android WebRTC

世界之书:《麦田里的守望者》与在虚无中创造希望

lidaobing

麦田里的守望者 28天写作

发布会直播技术及业务实践

vivo互联网技术

分布式 服务器 直播技术

Gridea+GitHub搭建个人博客

Simon

GitHub Pages 博客 七日更

谁告诉你观察者就是发布订阅模式的!抽他!

爱笑的架构师

设计模式 23种设计模式 观察者模式 Java设计模式 七日更

甲方日常72

句子

工作 随笔杂谈 日常

【经验分享】遵循10步法,应用系统发布效率大不同!

嘉为蓝鲸

敏捷 运维自动化 部署 发布流程 应用发布

MSHA x Chaos 容灾高可用实践

阿里巴巴云原生

数据库 高可用 云原生 中间件 容灾

盘点 2020 | 鲜衣怒马少年时,不负韶华行且知!

程序员的时光

程序员 成长 编程之路 计算机 盘点2020

智慧社区综合信息服务平台搭建,智能社区建设解决方案

t13823115967

智慧社区系统开发

智慧警务大数据可视化平台搭建,警情分析研判系统

135深圳3055源中瑞8032

距离 Java 开发者玩转 Serverless,到底还有多远?

阿里巴巴云原生

Java Serverless 微服务 云原生 中间件

编程之美!从线程池状态管理来看二进制操作之美

洋仔聊编程

Java 源码分析 线程池

Service Mesh最火项目Istio是怎么做流量管理的?

AI乔治

Java 架构 istio

入门参考:从Go中的协程理解串行和并行

soolaugust

go Go Concurrency Patterns 七日更

【理论篇】浅析分布式中的 CAP、BASE、2PC、3PC、Paxos、Raft、ZAB

merlinfeng

大数据 分布式

Polkadot系列(三)——如何实现共享安全性

QTech

区块链 polkadot 跨链

Windows安装MySQL5.7教程

Simon

MySQL windows 安装 七日更

智慧公安重点人员管控系统开发,预警研判系统搭建

135深圳3055源中瑞8032

脑洞:如何用一个整数来表示一个列表?

Python猫

Python

得物(毒)APP,8位抽奖码需求,这不就是产品给我留的数学作业!

小傅哥

Java 小傅哥 编程开发 七日更 数学逻辑

智慧城市平安智慧社区平台建设,公安防控管理平台

WX13823153201

2020 微信头像圣诞帽来啦,快给 TA 戴帽子吧~

mghio

圣诞帽 微信头像 圣诞节

Fair World智能合约APP系统软件开发

开發I852946OIIO

系统开发

向我看齐!京东智联云成 2020 TOP100 Summit“技术标兵”

京东科技开发者

DevOps 云原生 数字化

区块链食品溯源--为食品安全保驾护航

135深圳3055源中瑞8032

使用基于 SpringMVC 的透明 RPC 开发微服务

AI乔治

Java spring 架构 微服务 Spring Cloud

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

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