《验收测试驱动开发:ATDD实例详解》—第2章2.1节第一个测试用例

    xiaoxiao2024-02-23  119

    本节书摘来自异步社区《验收测试驱动开发:ATDD实例详解》一书中的第2章2.1节第一个测试用例,作者【德】Markus Gärtner,更多章节内容可以访问云栖社区“异步社区”公众号查看。

    第2章 代客泊车的测试自动化验收测试驱动开发:ATDD实例详解团队决定从表1-11所示的停车场故事的代客泊车的实例开始做。大家决定使用Cucumber1来实现测试自动化。Cucumber使用Ruby语言将实例的数据表示和被测试系统粘合在一起。在Cucumber中,每个测试集合被称为一个特性(feature),每个特性由一个单独的文本文件来描述。

    为了使用Cucumber来实现测试自动化,我们需要一组特性来记录测试数据,一些用来描述与被测应用交互的测试步骤定义,以及一套环境设置信息。

    Tony脑海中的总体架构如图2-1所示。

    最顶端的那些实例是在讨论会中确定的。现在,Tony开始将它们导入Cucumber。Cucumber需要一些粘合代码以执行被测应用。粘合代码可以分为步骤定义、支持代码和第三方库(例如Selenium中驱动浏览器的代码库)。

    Tony计划将停车费计算器中的支持代码放入一个单独的库中,以便步骤定义中的粘合代码可以使用这个库。

    自动化测试环境的建立需要使用Selenium2。自动测试代码通过Selenium可以驱动浏览器,使得自动测试代码可以与网页交互并且验证取值的正确性,例如停车费用的计算结果。团队建立了一个持续集成系统,并连接了一个Headless Selenium服务器3。这样测试代码就可以在构建中连接服务器了。

    Headless Selenium服务器在一个虚拟的服务器上运行浏览器。使用Headless Selenium服务器,你可以在一个没有显示器的机器上运行所有的测试。本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

    2.1 第一个测试用例验收测试驱动开发:ATDD实例详解Tony首先选了“30分钟”这个用例。他开始在Valet.feature文件中描述这个代客泊车的特性。Tony在开讨论会用的桌边完成了他的第一个测试(见程序清单2-1)。

    程序清单2-1 Tony的第一个代客泊车测试

    1 Feature: Valet Parking feature 2  The parking lot calculator calculates costs for Valet    Parking. 3 4  Scenario: Calculate Valet Parking Cost for half an hour 5   When I park my car in the Valet Parking Lot for 30 minutes 6   Then I will have to pay $ 12.00

    现在,代客泊车特性有了第一个“30分钟”的测试。期望的停车费是12美元,就像在讨论会中确定的一样。在程序清单2-1所示的代码中,第1行描述了我们要测试的特性的名字。第2行是进一步的说明。Cucumber运行时会在控制台显示这个说明。Tony通常在这里交代他的测试意图,以便后来的测试编写人员理解,当然,那个人很可能就是几个月后的他自己。

    在第4行中,他把第一个测试命名为“计算半小时的代客泊车费用”。由于这个测试目标就是停车30分钟,这个名字取得很确切。第5~6行用到了关键字When和Then,以便描述测试的两个不同阶段。

    关键字When描述了触发被测系统运转应该采取的动作。可能包括调用一个函数,或者按下某个按键。

    车位类型以及停车时间是When关键字的参数。Cucumber将会解析这些参数,并将它们提供给系统,这样应用程序就可以计算停车费用了。

    Then关键字描述了系统执行应用程序之后测试的后置条件,任何期望的结果都应该写在这里。在这个例子里,Tony加入了停车费计算结果的检测。

    现在,Tony保存Valet.feature文件,然后通过命令cucumber Valet.feature让Cucumber运行它。运行结果如程序清单2-2所示。结果首先输出了Tony刚才编辑的测试场景代码。在第8行中,运行结果提醒Tony Cucumber试图运行一个场景,但是这个场景未定义。同样在第9行中,Cucumber指出有两个未定义的步骤。从第12行开始是一个提示,提醒Tony如何实现这些未定义的步骤。

    程序清单2-2 第一个代客泊车测试的命令行输出

    1 Feature: Valet Parking feature 2  The parking lot calculator can calculate costs for Valet     Parking. 3 4  Scenario: Calculate Valet Parking Cost     # Valet.feature:4 5   When****I park my car in the Valet Parking Lot for 30 minutes     # Valet.feature:5 6   Then I will have to pay $ 12.00     # Valet.feature:6 7 8 1 scenario (1 undefined) 9 2 steps (2 undefined) 10 0m0.001s 11 12 You can implement step definitions for undefined steps with these snippets: 13 14 When /^I park my car in the Valet Parking Lot for (\d+)minutes$/ do |arg1| 15  pending # express the regexp above with the code you wish you had 16 end 17 18 Then /^I will have to pay \$ (\d+)\.(\d+)$/ do |arg1, arg2| 19  pending # express the regexp above with the code you wish you had 20 end 21 22 If you want snippets in a different programming language, just make sure a file 23 with the appropriate file extension exists where cucumber looks for step     definitions.

    遵照Cucumber的提示,Tony新建了一个名为Valet_steps.rb的文件,编写步骤定义,并将提示里的示例代码复制、粘贴到这个文件中。为了将测试数据和在被测系统上执行命令的粘合代码分开,Tony将支持代码(步骤定义)放在一个新建的目录step_definitions下的一个文件中。

    Tony在提示的代码桩(stub)上做了一些修改。这样在以后把第一个测试实例扩展到不同停车时长的时候会比较方便。修改结果在程序清单2-3中给出。

    程序清单2-3 第一个测试最初的步骤定义

    1 When /^I park my car in the Valet Parking Lot for (.*)$/ do     |duration| 2  pending 3 end 4 5 Then /^I will have to pay (.*)$/ do |price| 6  pending 7 end

    Cucumber可以从测试的描述语句中分析出变量,解析停车时长和费用就用到了这个特性。pending也是Cucumber可识别的关键字,运行时会输出信息,说明这个测试当前是挂起的,很可能因为这个测试正在编写中。现在步骤定义已经就位,当Tony重新执行这个测试时,得到了输出如程序清单2-4所示。

    程序清单2-4 加入了步骤定义的第一个代客泊车测试的输出

    1Feature: Valet Parking feature 2  The parking lot calculator can calculate costs for Valet     Parking. 3 4  Scenario: Calculate Valet Parking Cost     # Valet.feature:4 5   When I park my car in the Valet Parking Lot for 30 minutes     # step_definitions/Valet_steps.rb:1 6    TODO (Cucumber::Pending) 7    ./step_definitions/Valet_steps.rb:2:in '/^I park my car     in the Valet Parking Lot for (.*)$/' 8    Valet.feature:5:in 'When I park my car in the Valet     Parking Lot for 30 minutes' 9   Then I will have to pay $ 12.00     # step_definitions/Valet_steps.rb:5 10 11 1 scenario (1 pending) 12 2 steps (1 skipped, 1 pending) 13 0m0.002s

    为了能让被测系统运行,Tony还需要配置一下Web浏览器的驱动程序Selenium。Selenium有一个服务器组件,还有一个客户端库,供支持代码驱动浏览器及浏览网页。Tony在单独的env.rb文件中编写了支持代码,这样支持代码就可以和操作被测系统的粘合代码分离开了。他把这个文件放入了一个新建的etc文件夹(文件目录结构见图2-2)。他需要的每样东西都列在程序清单2-5中。

    程序清单2-5 设置Selenium客户端的支持代码 1 require 'rubygems' 2 gem 'selenium-client' 3 require 'selenium/client' 4 require 'spec/expectations' 5 require 'lib/parkcalc' 6 7 # _before all_ 8 selenium_driver = Selenium::Client::Driver.new \ 9  :host => 'localhost', 10  :port => 4444, 11  :browser => '*firefox', 12  :url => 'http://www.shino.de/parkcalc', 13  :timeout_in_second => 60 14 selenium_driver.start_new_browser_session 15 $parkcalc = ParkCalcPage.new(selenium_driver) 16 17 # _after all_ 18 at_exit do 19  selenium_driver.close_current_browser_session 20 end

    这段支持代码使用了Selenium提供的库,启动一个浏览器,然后打开ParkCalc页面。当所有的测试运行完后,它还会关闭浏览器窗口。

    文件env.rb同样需要一个库文件lib/parkcalc来进行停车费的计算。在开发测试的同时,Tony将会逐步地完善这个库文件。文件的初始内容如程序清单2-6所示。

    程序清单2-6 初始的ParkCalcPage类

    1 class ParkCalcPage 2  attr :page 3 4  def initialize(page_handle) 5   @page = page_handle 6   @page.open '/parkcalc' 7  end 8 end

    在初始化函数中,传入的page_handle存入本地属性page中,并打开了/parkcalc页面。这段初始化代码,完成了用传入的浏览器打开Web表单的功能。传入的浏览器正是在env.rb中配置的那个。

    Tony开始分步开发他的测试。他需要完成的第一步是向Web接口填入停车时长参数。Tony现在还不知道具体的实现细节,但是他已经看过手绘的页面布局的最终设计。When I park my car in the Valet Parking Lot for< duration >语句包含两个基本步骤。首先,选择正确的停车场。其次需要填入代表正确停车时长的值。Tony打算根据自己的主观期望来处理这个特定的问题,他在Valet_steps.rb中写下了运行这个实例所需用到的API(见程序清单2-7)。

    程序清单2-7 对第一个语句的预想实现

    1 When /^I park my car in the Valet Parking Lot for (.*)$/ do    |duration| 2  $parkcalc.select('Valet Parking') 3  $parkcalc.enter_parking_duration(duration) 4  pending 5 end

    第一步对应程序清单2-7的第2行。Tony认为应该有个停车场的选择机制。根据实现细节,这可能意味着在文本框中输入字符串,或者从选择组合框中选中正确的停车场,又或者从一个下拉菜单中选择停车场。由于目前Tony对此还一无所知,他将这个具体实现的细节推迟到ParkCalcPage类被实现。

    第二步就是程序清单2-7的第3行,它描述了停车时长通过某种方式填入了页面。同样,这也许意味着在输入框中输入一个时长字符串,也许有两个输入框分别对应停车起始时间和结束时间,然后根据输入计算出时长。由于具体的用户界面实现方式还没有敲定,Tony会等到ParkCalcPage类实现后再做决定。

    为了表明这些测试步骤定义还未完成,Tony在语句定义的最后加上了关键字pending。这样一旦另外两个关键字实现了就能提醒未来的实现者修改步骤定义。

    现在Tony为他期待实现的两个方法提供空的实现,通过这种方式来向ParkCalcPage类未来的实现者告知他对这个类接口的设计(见程序清单2-8)。

    程序清单2-8 为ParkCalcPage类加上空的实现

    1 class ParkCalcPage 2  attr :page 3 4  def initialize(page_handle) 5   @page = page_handle 6   @page.open '/parkcalc' 7  end 8 9  def select(parking_lot) 10  end 11 12  def enter_parking_duration(duration) 13  end 14 15 end

    Tony给ParkCalcPage添加了select(parking_lot)和enter_parking_duration(duration)方法。前一个以后用来选择停车场,后一个负责填入用户界面提供的停车时长。

    Tony现在集中注意力处理他测试中的验证步骤。同准备步骤一样,Tony同样根据自己的主观期望来表述停车费的最终验证方法。程序清单2-9中展示了他对step_definitaions/Valet_steps.rb文件的修改。对ParkCalcPage类必要的修改见程序清单2-10。

    程序清单2-9 基于预想的第一个语句的初始实现

    1 Then /^I will have to pay (.*)$/ do |price| 2  $parkcalc.parking_costs.should == price 3  pending 4 end

    程序清单2-10 停车费计算步骤的空实现

    1  def parking_costs 2   return nil 3  end

    Tony完成了他目前能够驱动的第一个测试。现在他面临着如何继续的抉择。一方面,他可以继续完成代客泊车需求讨论会中确定的其他实例。另一方面,他也可以与开发人员结对去对实例进行自动化,以便驱动这个特性之后的开发。第三种选择是继续应付剩下的4种停车场,并且在Cucumber中写好它们各自的第一个测试。Tony决定和开发人员结对去实现第一个代客泊车功能并且对他的第一个测试进行自动化。这样,Tony可以获得他对目前已完成工作的及时反馈,稍后就能继续开发其他测试了。这么做还有一个好处,即Tony不会在实现开始之前加入太多无法通过的测试。

    本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

    相关资源:七夕情人节表白HTML源码(两款)
    最新回复(0)