BT

你的观点很重要! 快来参与InfoQ调研吧!

自动化测试的分层结构

| 作者 李贝 关注 0 他的粉丝 发布于 2009年10月26日. 估计阅读时间: 23 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

A note to our readers: As per your request we have developed a set of features that allow you to reduce the noise, while not losing sight of anything that is important. Get email and web notifications by choosing the topics you are interested in.

摘要

在测试自动化中,测试代码中不仅仅包含测试逻辑,还包含许多其他代码,比如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中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

学习 by Chen Lara

的确提出了自动化的现存问题。。但是解决方案在目前中国来说实施比较困难

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT