All About Smart Testing

软件开发者面试百问 之 软件测试

上一篇 / 下一篇  2010-03-10 12:55:23

InfoQ翻译了Jurgen Appelo的“软件开发者面试百问”,该文受到了许多读者的欢迎。在2009年5月刊的《程序员》中,周伟明先生对其中的“项目管理”和“软件测试”部分的题目进行了回答。笔者不才,也想试着给出自己的答案。既然是“面试”,那么作答时,便不参考其他资料,只凭记忆和经验撰写。

1. 什么是回归测试?怎样知道新引入的变化没有给现有的功能造成破坏?Do you know what a regression test is? How do you verify that new changes have not broken existing features?

回归测试是指,运行一组测试用例(称作“回归测试用例集”)来检查代码的变化没有破坏已有的功能。软件开发是一个演进的过程,需要不停地重构和增加代码。回归测试提供了修改的正确性反馈,为稳定的开发流程提供了安全网。在理想的情况下,应该将所有测试用例组成回归测试用例集,来检查修改的正确性。不过,受到资源的限制,开发者往往选择与修改相关的测试用例来运行回归测试。例如,被修改类的所有单元测试用例、被修改模块的所有组件测试用例、被修改系统的所有功能性测试用例等。

在我的工作中,我通过以下方法来检查代码变化的正确性。

  • 利用每小时一次的rolling test来检查最近的签入(check-in)没有造成构建失败(build break)、部署失败、端到端系统测试(end-to-end system test)失败。这可以及时地发现阻碍每日测试的blocking bug。
  • 利用每日测试(daily test)来运行所有的自动化测试用例集。
  • 通过代码比较工具,检查前后版本的代码。一方面,通过代码检查来挖掘可能的问题;另一方面,可以有针对性地补充一些测试用例,以覆盖新的逻辑。
  • 在恰当的时候,再次运行手工测试用例集。由于我的测试用例集的自动化程度较高,我通常在里程碑(milestone)快结束前运行手工测试用例集。
  • 代码变更往往造成测试失败(test break),需要重构测试代码来适应新的接口和实现。采用良好的测试实现模式,可以降低测试维护的代价。

2. 如果业务层和数据层之间有依赖关系,你该怎么写单元测试?How can you implement unit testing when there are dependencies between a business layer and a data layer?

严格地说,单元测试不应该访问外部资源(包括文件、网络、数据库等)。单元测试的目的是提供快速的正确性反馈,使“测试—开发—重构”的迭代循环达到“流”状态。依赖于外部资源的单元测试运行缓慢,可能给开发带来负面影响。如果测试运行超过1分钟,许多时间被浪费在等待上,开发者很难到达“流”状态,开发效率会隐式地降低。如果测试运行超过5分钟,开发者可能会选择偶尔运行单元测试,以加速开发速度。这将大大削弱单元测试作为安全网的效果,进一步降低开发效率。

如果业务层和数据层有依赖关系,开发者还是应该尽量使单元测试不访问外部资源。我会使用以下策略来达到这一目标。

  1. 在设计上,将业务层和数据层的依赖限制在最小范围内。一个可能的方案是,将依赖封装在一个类中,例如DataManager。在业务层,只有少量代码依赖于它,而大多数代码不依赖于数据层。对于数据层无关的业务层代码,可以方便地编写单元测试。
  2. 对于依赖于DataManager的类,可以采用伪对象(mock object)测试模式来编写单元测试。对于C#等静态语言,需要使用提取借口(extract interface)的重构手法,获得DataManager的接口IDataManager。然后编写符合该接口的伪对象,例如DataManagerMock。DataManagerMock不访问数据库,它使用内存中的数据来模拟数据层操作。测试方法用依赖注入(dependence injection)策略,使被测试的业务对象依赖于受控的DataManagerMock,这样就可以编写只访问内存对象的单元测试了。以下代码演示了这种方法的思路。
    1:// Business class under test
    2:classEmployee {
    3:     IDataManager data;
    4:     Business(IDataManager data) {
    5:this.data = data;
    6:     }
    7:     ...
    8: }
    9:
    10:// Mock object
    11:classDataManagerMock : IDataManager {
    12:// database in memory
    13:     Hashtable employees =newHashtable();
    14:     ...
    15: }
    16:
    17: [TestMethod]
    18:voidTestEmployee() {
    19:// setup
    20:     IDataManager data =newDataManagerMock();
    21:     Employee e = Employee(data);
    22:// test object e with data
    23:     ...
    24: }
  3. 除了伪对象,还可以采用测试专用子类(test specific subclass)、依赖注册与查找(dependence register and lookup)、动态类型(duck typing)等策略,使业务对象在测试方法中不依赖于DataManager,而是依赖于不访问数据库的伪数据层对象。这里的关键是,解除业务层与数据层的依赖,在测试方法中,将数据层对象替换为测试专用对象(test specific object)。
  4. 有时,访问数据库是难以避免的。在这种情况下,应该为每一位开发者和测试者建立数据库沙箱(sand-box),使得他们的工作相互独立。在测试方法中,先用数据操作脚本将数据库恢复到指定状态,然后再执行测试逻辑。通常,我会从产品数据库中选择少量数据,构成一个典型的数据集,用于初始化数据库。这样既体现了产品数据库的特性,也尽可能地提高了测试速度。

3. 你用哪些工具测试代码质量? Which tools are essential to you for testing the quality of your code?

在实际工作中,我通过以下工具来检查代码质量。

  • 单元测试工具。当我编写IronPython代码时,我使用自己编写的单元测试框架IpyUnit。它只依赖于.NET Framework 2.0,并适用于IronPython 1x和IronPython 2x。对于C#代码,我使用VSTS Unit Test Framework。对于C++代码,我使用过CppUnit,后来使用一个自己编写的单元测试框架,其实现思路与CppUnitLite相似。自我辩解一下:我没有重复发明轮子的倾向。编写IpyUnit是因为现有的Python单元测试框架依赖于CPython程序库,而我需要一个只依赖于.NET Framework的测试框架。编写自己的C++单元测试框架是受到一些公司政策的要求。
  • 代码静态分析工具。Herb Sutter在《C++ Coding Standards》中建议在高警告水平做干净的编译(clean build)。因此,我会努力使我的代码没有编译警告。此外,我还使用Visual Studio内建的FxCop来检查.NET程序的质量。
  • 代码度量工具。我写过一个Python脚本,扫描代码树(source tree)获得每一个模块(对应为代码树上的目录)有多少文件、有多少行代码。这是一个非常简单的工具,可以提供代码规模与分布的概要信息。利用这些信息,可以为测试计划的制定提供指导。例如某个模块的代码很多,测试却相对较少,那么应该考虑为它增加更多的测试用例。Visual Studio 2008 内建了代码复杂度度量工具,也可以为测试提供指导。例如,某个类的复杂度较高,测试却相对较少,那么应该考虑为它增加测试用例。
  • 自制工具。在性能测试、压力测试等活动中,需要自己编写一些工具来完成任务。例如,编写一个客户端工具,它可以快速地向服务器提交大量的请求。这有助于发现一些在压力情况下才可能出现的问题。

4. 在产品部署之后,你最常碰到的是什么类型的问题? What types of problems have you encountered most often in your products after deployment?

在我的项目中,产品部署之后常遇到两个问题。第一、客户认为系统的一些功能不满足需求。这是一个普遍却严重的问题,我将在问题9的回答中讨论缓解方法。第二、在大负载压力下,系统出现不稳定的症状,例如系统响应迟缓、服务重启等。对于该问题,我考虑采用以下手段来缓解。

首先,了解系统在产品环境(production environment)中的工作负载(work load)。通过检查已有系统的日志,可以预测出新系统上线之后的工作负载。基于历史数据的计算,而不是猜测,是预测准确的关键。有时候,新系统提供了新的业务,没有历史数据可以分析。开发者需要与客户密切合作,根据客户的业务需求来推测可能的负载。也可以评估类似系统的负载,以推测被测试系统的负载。总之,这不是一项简单的任务,最好用多种方式进行预测,然后综合得到系统的预期负载。

然后,根据预期负载,设置性能测试的测试目标。在确立目标之后,可以进行下列测试来质疑(critique)被测试系统。

  • 性能测试(performance test):检查系统能否达到性能目标,能否在预期最大负载下正常工作。所谓正常工作是指,系统的响应时间、吞吐量、资源占用、结果正确性等指标符合预期。
  • 负载测试(load test):考察系统的性能指标随工作负载变化的情况,以检查系统的可伸缩性(scalability)。
  • 压力测试(stress test):使用远远大于最大预期负载的工作量来测试系统。在这种压力下,系统会响应迟缓或丢失连接,但不应该发生资源泄漏、服务停止等无法挽回的问题。当负载落回正常范围之后,系统的性能指标应该恢复正常。

在测试过程中,应该使用多种手段监控系统的运行情况。一旦发现问题,如CPU资源占用过高,就应该展开分析。性能测试是一个持续的过程,从开发早期一直延续到产品部署。及时地发现问题并解决问题是性能测试成功的关键。

这世界上的许多事情都是知易行难。反思最近一次产品开发,也是在以上几点有所不足。第一、没有准确地预测出产品的实际负载。第二、在测试过程中,没有模拟出一些在产品环境会发生的使用情景,使得有些问题在产品环境中才发现。第三、没有从整体监控整个系统的性能。对于分布式计算系统,不但需要检查子系统的性能,还需要从整体考察系统的吞吐量与瓶颈。测试是一个学习的过程,希望这些反思会对未来的工作有所帮助。

5. 什么是代码覆盖率?有多少种代码覆盖率?Do you know what code coverage is? What types of code coverage are there?

代码覆盖率用于评估测试用例覆盖代码的程度。常见的代码覆盖率包括语句块覆盖、分支覆盖、函数覆盖、类覆盖等。Visual Studio 2008已经支持这些覆盖率度量。

  • 语句块覆盖(block coverage)评估测试用例的执行覆盖了多少语句块。语句块是最大的单入口、单出口的顺序语句分组。
  • 分支覆盖(branch coverage)评估测试用例的执行覆盖了多少分支。如何统计分支,有不同的计算方法。一般可以将语句块之间的可执行连接视作分支。
  • 函数覆盖(function coverage)评估测试用例的执行覆盖了多少函数。
  • 类覆盖(class coverage)评估测试用例的执行覆盖了多少类。

当我还在大学的时候,研究过基路径覆盖(basis path coverage)、条件覆盖(condition coverage)、决定性条件覆盖(MC/DC)等覆盖率度量。不过,它们在工业界没有得到广泛的应用。

代码覆盖率可以检查出未被测试覆盖的代码,有助于揭示测试用例集的不足。在实际工作中,我会不断地补充测试用例,使得语句覆盖率尽可能达到100%。

6. 功能性测试和探索性测试的区别是什么?你怎么对网站进行测试?Do you know the difference between functional testing and exploratory testing? How would you test a web site?

功能性测试侧重于检查系统是否符合规格说明所指定的功能性需求。开发者可以根据规格说明,构造相应的测试用例,以检查系统是否实现了指定的特性(feature)。在敏捷团队中,开发者和客户(或商业分析师)一起开发用户故事(user story)。这些故事被实现为端到端的功能性测试(end-to-end functional test),用于驱动开发和验收成果。在敏捷开发中,大部分功能性测试应该是自动化的。

探索性测试是语境驱动学派(context driven school)所倡导的测试方法,强调根据当前测试的语境选择最有效的测试策略。测试者选择一个测试目标,一边测试系统,一边学习系统的知识。随着知识的积累,他会不停地调整测试策略,并使用多种方法来测试系统。当测试目标达成,或者他认为出现了更有价值的目标,测试者会改变测试目标,展开新的测试。探索性测试将测试视为一个不停地学习、尝试、反馈的过程,强调测试者的学习、思考、应变的能力。因此,探索性测试只能是手工测试。当然,测试者可以用自动化工具完成一些例行任务,例如安装软件、设置环境、准备数据等;他也可以开发自动化测试用例来复现探索性测试所发现的缺陷。

网站测试是一个复杂的问题。如果只考虑功能性测试和探索性测试,可以考虑以下测试活动。

  1. 将网站划分为表现层和业务层。业务层负责业务逻辑,接受表现层的输入,并提供表现层所需要的数据。表现层接受用户的输入,并将业务层的数据以合适的方式展现给用户。
  2. 开发者与商业分析师一起确定用户故事。开发者将其实现为自动化测试用例。与业务相关的功能,应该在业务层编写测试用例。与表现相关的功能,应该在表现层编写测试用例。通常,在业务层实现测试的投入产出比较高,因此应该尽量分离表示逻辑和业务逻辑,使表示层承担的任务尽可能的少。功能性测试用例提供了基本的质量保证,是进一步测试的基础。
  3. 在功能性测试相对稳定之后,可以开展探索性测试。可以用浏览器访问网站,也可以用客户端工具直接访问业务层。可以采纳情景测试(scenario test)的方法,模拟真实用户去完成一个复杂的任务或构建一个真实的应用。例如,如果测试在线文档处理网站,就用它去排版一篇包含多种字体、图形、格式的文档(收集一些博士学位论文可能是一个好主意)。也可以用工具扫描网站,以发掘安全漏洞,然后用具体的测试用例来证实潜在的漏洞是可以被利用的。总之,可以在功能性、易用性、安全性等多个角度探索系统。

7. 测试套件、测试用例、测试计划,这三者之间的区别是什么?你怎么组织测试? What is the difference between a test suite, a test case and a test plan? How would you organize testing?

测试计划(test plan)对测试活动进行整体规划。它从以下几个方面辅助测试工作的展开。

  • 为测试工作进行技术准备。测试计划应该给出测试目标和退出条件。在此基础之上,给出测试策略,以指导具体的测试活动。
  • 对资源进行分配。测试计划应该给出大致的测试日程,并根据日程分配软硬件资源。
  • 建立测试协作的基础。测试计划应该制定大致的测试过程(test process)、明确的测试规范(test guideline)和具体的人员职责。
  • 识别测试风险,进行风险评估,并建立风险监测和预防机制。

在我看来,测试计划最重要的功能是提供团队协作的基础,记录团队已经达成的共识。如果这些基础和共识发生了变化,那么测试计划也需要进行相应的修改。

测试用例(test case)是一个已文档化的流程(documented process),用于探测软件的缺陷。对于单元测试,一个测试用例最好只检查一项内容,例如一个方法的一个特性,这使得测试用例思路清晰、易维护、易运行。对于系统测试,一个测试用例往往实现了一个复杂的流程,该流程对应于一项用户活动,该活动实现了特定的用户价值。对于性能测试,一个测试用例往往体现为一个应用场景,以及在该场景下对性能指标的要求。可见,不同类型的测试用例有不同的表现形式。通常,它是该类型测试的最小组织单位。

测试套件(test suite)是测试用例的集合。测试者根据测试需求将测试用例组织到测试套件中,以便更有效地运行测试、生成测试报表。例如,将一个模块的所有测试组织到一个测试套件中,作为回归测试用例集。如果该模块发生变化,则运行该回归用例集,以捕获可能的回归错误。再例如,将按照功能特性(functional feature)划分测试套件,然后根据测试套件生成测试报表。所得的报表按照特性组织,便于阅读和理解。

在实际工作中,我按照如下方式组织自动化测试。

  1. 一个测试方法(test method)只测试一项内容。测试方法的命名规则是test_FeatureName_TestStartegy,例如test_CreateUser_NoneInput,这样可以快速地了解被测试的特性和所使用的测试策略。
  2. 将一个类或特性的所有测试用例都放在一个测试类(test class)中,这样便于按照特性审查、维护、增加测试用例。
  3. 将一个测试夹具(test fixture)的所有测试用例都放在一个测试类中。所谓测试夹具,是指完成测试所必需的被测试对象以及相关的业务对象、伪对象、桩对象等。在一些情况下,需要大量的代码来建立测试夹具。因此,按夹具来组织测试方法,可以复用夹具建立代码,提高测试开发效率。
  4. 将可复用的测试代码组织到测试辅助类(test utility class)或父测试类(super test class)中。
  5. 按照功能特性组织测试套件。将一个特性所有的测试用例都放置在一个测试套件中。将所有的测试套件组成测试用例全集,用于每日测试。此外,我还组织了一个BVT suite,用于每小时一次的rolling test。

8. 要对电子商务网站做冒烟测试,你会做哪些类型的测试? What kind of tests would you include for a smoke test of an ecommerce web site?

冒烟测试用于快速地检查构建(build)的质量,发现阻碍进一步测试的严重缺陷。在冒烟测试中,我倾向于运行若干系统测试,以检查系统的工作流是否运行正常。这是基于这样一条前提:开发者在签入代码之前已经运行过单元测试。这时,错误更有可能发生在组件之间、在子系统之间。

对于电子商务网站,我会运行以下测试。

  1. 在一台干净的机器上,完整地部署网站。部署应该成功。
  2. 一个已注册的客户可以下订单。测试用例可以完成如下操作:使用一个内建的测试账户登录系统,选择一件商品加入购物车,结算购物车中的商品,得到订单确认信息。
  3. 后台系统可以完成订单流程。测试用例会检查后台系统正确地完成了如下操作:从数据库中获得订单,修改订单状态为“接受”,进入配货阶段,进入发货阶段,修改订单状态为“完成”。

在我的冒烟测试中,测试用例关都注于最重要的正常流程(happy path),更复杂和全面的测试留待后续测试活得完成。以上测试考察了订单处理的流程。如果需要,还可以补充一些系统测试用例,以检查用户注册、用户评论、商品推荐、后台统计报表生成等功能。

9. 客户在验收测试中会发现不满意的东西,怎样减少这种情况的发生? What can you do reduce the chance that a customer finds things that he doesn't like during acceptance testing?

用户在验收测试测试中表示不满意,这通常是因为软件没有满足他的需求。以下手段有助于更好地满足用户需求。

  1. 在捕获初始需求时,搞清楚用户的业务目标。许多时候,开发者混淆了用户的目标和任务,一方面没有提供有力的业务支持,另一方面设置了条条框框,使得用户难以方便地实现业务价值。 
  2. 在规格说明中,排除催眠词汇。所谓催眠词汇是指“有效”、“快速”、“友善”、“加速”等让人舒服又不知所云的修饰语。它们应该被量化的陈述所取代:对于10个并发连接,每一个请求的响应时间平均为1ms,最大不超过2ms。
  3. 用户不会明确地提出,但是他们认为:网站就应该有流畅的反应、他们的在线交易就应该是安全的、他们的隐私就应该受到保护。因此,开发者需要参考类似应用和相关标准,去捕获用户的隐式需求。
  4. 应该尽早地获取用户对人机交互的反馈。有人用HTML或电子幻灯片向用户展示界面;这种方法通常难以体现用户与软件的交互过程。也有人构建原型系统,供用户试用;这种方法需要较长的准备时间,可能不够快速。我倾向于用白纸、即时贴、画笔等构建低技术的软件界面,然后让用户试用该界面去完成一个特定的目标。在这个过程中,我充当界面的后台系统,根据用户的输入,对界面做出修改,以模拟软件响应。这种方法成本低、速度快,是交互设计的好帮手。
  5. 敏捷开发通常以3~6周为单位进行迭代。在每一个迭代之后,都应该将软件发布给用户,或者邀请用户来试用软件。根据用户的反馈来规划下一个周期的开发任务。
  6. 敏捷开发将“现场客户”作为一条实践方法。这看上去很困难,实际上未必。例如,可以邀请客户定期来访,或定期拜访用户,以获取用户反馈。有些团队成员可能有相关的业务背景,他们可以充当现场客户的角色。

总体而言,这是一个思维方式的问题。有些软件开发者以技术为中心、以自我感觉为中心,不太在意用户的目标和感受。即便提供了用户的迫切要求,他们也可以用种种借口搪塞过去——这就是Alan Cooper所谓的“技术人”的特点。只有真正以客户为中心,以上方法才有可能获得成效。

10. 你去年在测试和质量保证方面学到了哪些东西?Can you tell me something that you have learned about testing and quality assurance in the last year?

在过去的一年,我从理论和实践上都有所收获。在理论上,我阅读了多本软件测试与质量方面的书籍。给我留下深刻映像的是:

  • Agile Testing》提出的测试四象限。它利用technology-facing, business-facing, support team, critique product这四个维度,对测试进行了分类。这是一种新的测试思考方法,对于测试活动的选择很有帮助。
  • 持续集成》提出的持续地交付价值流的观点。根据一年来在rolling test中的实践,我切实地体会到它的重要性。
  • 敏捷软件开发》提出的敏捷思维集合(mind set)、原则和方法。这是一本需要反复阅读并实践的好书。
  • xUnit Patterns》提出的测试模式和重构方法。目前,我在工作中有意识地应用这些模式,获得了不错的效果。
  • 公司提供的User Experience Design的培训。它让我认识到人机交互和低技术原型的重要性

在实践上,我也有一些积累。

  • 实践了类似于持续集成的rolling test
  • 学习并在工作中应用了IronPython。利用IronPython完成了大部分的测试自动化工作,从而较好地掌握了Python语言,并认识到动态语言对于提高测试效率的积极意义。
  • 负责系统的性能和压力测试,并在这一过程中学习了《Web应用程序性能测试指南》。问题4的回答表明我的工作还有许多不足,需要进一步改进。
  • 在工作中积极反思,总结了一种探索式的自动化测试开发方法。

 

后记:花了许多时间写出这些回答。一些概念性的问题(如问题1, 5, 6, 7)看似简单,但是若要给出准确的答案,也颇费脑力。这也许是“命题作文”的好处吧:强迫你思考一些自以为熟悉的概念,从而获得新的认识。在回答的过程中,平日的积累发挥了重要的作用。此外,以上所有问题都是开放式的。在真实的面试过程中,面试官会根据答案做进一步的刺探(prod)。一问一答的过程将激荡出更多的灵感,此间精彩非答卷式的文字可以比拟。


TAG:

 

评分:0

我来说两句

Open Toolbar