《验收测试驱动开发:ATDD实例详解》—第2章2.2节结对完成第一个测试

简介:

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

2.2 结对完成第一个测试
验收测试驱动开发:ATDD实例详解
Tony和Alex开始结对写实现代码并对第一个测试进行自动化。Alex已经为网站做好了第一个布局,他向Tony介绍了他的想法。

Alex:嘿,Tony,你有兴趣看看停车费计算器的进展吗?

Tony:其实我想跟你结对儿,对第一个测试进行自动化。

Alex:哦,太好了,你从哪个开始的?

Tony:代客泊车,我花了30分钟引入了第一个测试,过来之前我刚把第一个挂起的实例提交了。

Alex:很好,我先给你看看我这边的完成情况。

Alex给Tony展示了他对页面布局的初始设计(见图2-3)。


7785e1f76bf29a8a12f7d1c77e096727c606e021

2.2.1 初始化
Alex:我设计由下拉菜单展现不同的停车场。日期可以直接由输入框填入字符串,或者用日历控件选择日期。停入和离开的时间由文本输入,有一个单选按钮决定上午还是下午。当计算按钮按下之后就会显示估算的停车费。

Tony:看起来不错。看看这个,现在这些步骤定义还是挂起的,我们需要解决它们。

Alex:好的,这些看起来并不复杂。我们从下拉菜单选择停车场开始吧。这里我用的id是ParkingLot,所以从下拉菜单选择合适的值一步就可以做到,就像这样。

Alex实现了lib/parkcalc.rb文件中的select方法,如程序清单2-11所示。

程序清单2-11 从下拉菜单中选择正确的停车场

1  def select(parking_lot)
2   @page.select 'ParkingLot', parking_lot
3  end

Tony:好的,看起来很直观。我从id为ParkingLot的元素上选择了传入参数代表的停车场。很好,那么输入停车时间呢?

Alex:这需要更多的思考,我们用Ruby里的hash(散列表)来实现吧。以后你就可以扩展这些停入和离开的日期和时间值,然后模拟你所需要的所有不同的时长。

Tony:那咱们应该怎么做呢?

Alex:基本上我们根据传入的时长字符串查找实际的停入日期、停入时间、上午还是下午、离开日期、离开时间以及离开是上午还是下午的值,并将它们填入页面。这6个值代表了我构造的表单中的6个项。不过我们先来定义这个散列表(hashmap)。

Alex在ParkCalcPage类的开头构造了一个时长的散列表(见程序清单2-12)。

程序清单2-12 记录时长和实际日期时间的散列表

1  @@durationMap = {
2   '30 minutes' => ['05/04/2010', '12:00', 'AM', '05/04/2010'
    , '12:30', 'AM']
3  }

Tony:两个@说明durationMap是一个类变量,对吧?

Alex:对的,现在我们要从散列表中得到我们关心的那6个值,我演示给你看如何从散列表中取值。

Alex开始实现enter_parking_duartion函数(见程序清单2-13)。

程序清单2-13 从durationMap得到表单的6个参数

1  def enter_parking_duration(duration)
2   startingDate, startingTime, startingTimeAMPM, leavingDate,
    leavingTime, leavingTimeAMPM = @@durationMap[duration]
3  end

Alex:现在,让我们把这些值填入表单。我们从停入日期和时间开始。

如程序清单2-14所示,Alex继续扩充这个方法,填入停车的时长。

程序清单2-14 将停入日期和时间填入对应的表单项

1  def enter_parking_duration(duration)
2   startingDate, startingTime, startingTimeAMPM, leavingDate,
    leavingTime, leavingTimeAMPM = @@durationMap[duration]
3   @page.type 'StartingDate', startingDate
4   @page.type 'StartingTime', startingTime
5   @page.click "//input[@name='StartingTimeAMPM' and @value
   ='%s']" % startingTimeAMPM
6  end

Tony:你能解释一下吗?我看不懂最后一行。

Alex:我解释一下这里的细节。首先我们以时长为键值从散列表中取出6个参数,然后我们填入相应的停入日期和时间。

Tony:对,这很直观。但是最后那行乱七八糟的东西是干什么的?

Alex:我用它来定位单选按钮。这是个xpath路径,表示单选按钮所在位置和它的值。它告诉驱动程序点击名为“StartingTimeAMPM”且值为输入参数的按钮。

Tony:我希望能把它放在别的地方。它包含太多技术细节,不适合放在这个相对抽象的函数里。

Alex:我想你是对的,Tony。我们把它记下来,先完成这个函数再说。离开日期和时间还没有呢。和停入时间类似。不过我们先看看现在能否正常运行。

Alex开始执行这个测试,Alex和Tony一起看着浏览器窗口被打开,进入停车场计算页面,填入停车场、停入日期和时间值。最后浏览器被关闭,运行结果显示出来。

Tony:看起来不错,我们继续处理时长吧。我们还需要填入离开日期和时间。

Alex:当然,代码和填入停入日期和时长类似。我们把代码复制、粘贴过来,然后改一下变量名。如果这能运行,我们再回过头来清理代码。

Alex继续向函数中添加代码,填入离开日期和时间(见程序清单2-15)。

程序清单2-15 在刚才的函数中加入离开日期和时间

1  def enter_parking_duration(duration)
 2   startingDate, startingTime, startingTimeAMPM, leavingDate,
    leavingTime, leavingTimeAMPM = @@durationMap[duration]
 3   @page.type 'StartingDate', startingDate
 4   @page.type 'StartingTime', startingTime
 5   @page.click "//input[@name='StartingTimeAMPM' and @value
    ='%s']" % startingTimeAMPM
 6
 7   @page.type 'LeavingDate', leavingDate
 8   @page.type 'LeavingTime', leavingTime
 9   @page.click "//input[@name='LeavingTimeAMPM' and @value='%
    s']" % leavingTimeAMPM
10  end

Tony和Alex运行这些步骤,检查是否正确填入了离开日期和时间。

Alex:很好,能运行。我们现在来清理吧。这两部分代码看起来真的很类似。咱们把它放入一个单独函数中吧。

Alex和Tony写了一个新函数用来填写停入或离开的日期、时间,并逐步替换了原函数的实现。每做一点小的改动他们都会验证第一个测试还能正常运行。他们完成后,代码如程序清单2-16所示。

程序清单2-16 将填入停车时长抽取为单独函数

1  def enter_parking_duration(duration)
 2   startingDate, startingTime, startingTimeAMPM, leavingDate,
    leavingTime, leavingTimeAMPM = @@durationMap[duration]
 3   fill_in_date_and_time_for 'Starting', startingDate,
    startingTime, startingTimeAMPM
 4   fill_in_date_and_time_for 'Leaving', leavingDate,
    leavingTime, leavingTimeAMPM
 5  end
 6
 7  def fill_in_date_and_time_for(formPrefix, date, time, ampm)
 8   @page.type "%sDate" % formPrefix, date
 9   @page.type "%sTime" % formPrefix, time
10   @page.click "//input[@name='%sTimeAMPM' and @value='%s']"
    % [ formPrefix, ampm ]
11  end

Alex:现在,我们来看一下如何提取那些怪异的xpath路径。

Tony:把它声明为一个常量表达式如何?

Alex:我也是这么想的。同时我还想把其他固定的字符串也放入变量里,这样将来我们就可以很容易地修改它们了。我们一个一个来提取吧。我们先来消除掉xpath。我们需要给这个变量取个名字,你说叫什么好呢?

Tony:叫"amPMRadioButtonTemplate"如何?

Alex:我没意见。我们以后是不是可以把日期和时间字符串也放进timeTemplate和dateTemplate?

Tony:听起来不错。我们同样可以把前缀放入startingPrefix和leavingPrefix.

Alex:对的,我还想把停车场的ID也放入一个常量中去。

Tony:现在看起来就很好了。

程序清单2-17展示了ParkCalcPage类在Alex和Tony提取常量之后的最终版本。现在,初始化步骤就完成了。

程序清单2-17 ParkCalcPage初始化步骤的最终版本

1 class ParkCalcPage
 2
 3  @@lotIdentifier = 'ParkingLot'
 4  @@startingPrefix = 'Starting'
 5  @@leavingPrefix = 'Leaving'
 6  @@dateTemplate = "%sDate"
 7  @@timeTemplate = "%sTime"
 8  @@amPMRadioButtonTemplate = "//input[@name='%sTimeAMPM' and
    @value='%s']"
 9
10  @@durationMap = {
11   '30 minutes' => ['05/04/2010', '12:00', 'AM', '05/04/2010'
    , '12:30', 'AM']
12  }
13
14  attr :page
15
16  def initialize(page_handle)
17   @page = page_handle
18   @page.open '/parkcalc'
19  end
20
21  def select(parking_lot)
22   @page.select @@lotIdentifier, parking_lot
23  end
24
25  def enter_parking_duration(duration)
26   startingDate, startingTime, startingTimeAMPM, leavingDate,
     leavingTime, leavingTimeAMPM = @@durationMap[duration]
27   fill_in_date_and_time_for @@startingPrefix, startingDate,
    startingTime, startingTimeAMPM
28   fill_in_date_and_time_for @@leavingPrefix, leavingDate,
    leavingTime, leavingTimeAMPM
29  end
30
31  def fill_in_date_and_time_for(formPrefix, date, time, ampm)
32   @page.type @@dateTemplate % formPrefix, date
33   @page.type @@timeTemplate % formPrefix, time
34   @page.click @@amPMRadioButtonTemplate % [ formPrefix,
     ampm ]
35  end
36
37 end

2.2.2 检查结果
Tony:现在我们来检查一下输出。我们还没有点击你在表单里放的Calculate按钮,并且我们需要从页面中获取费用的计算结果。

Alex:当然,我们先把pending语句从步骤定义中去掉,这样我们的定义的Then部分就可以运行了。

Tony: 噢!我差点就把这个忘了,要不然我得花半天时间才能搞清出了什么毛病。

Alex:这就是结对的好处,不是吗?

Tony:那么现在我们可以把pending语句从检查中去掉,然后我们来实现它。

Alex:好的。现在我们来看看怎么检查正确的价格。首先,我们需要点击计算按钮。我想把这作为第一步加入parking_costs函数,放在返回费用之前。之后我们需要等待页面载入新的值。然后我们获取记录费用的元素并返回它。

Alex实现了parking_costs函数,见程序清单2-18。

程序清单2-18 检测代码的初始版本

1 def parking_costs
2  @page.click 'Submit'
3  @page.wait_for_page_to_load 10000
4  cost_element = @page.get_text "//tr[td/div[@class='SubHead']
    = 'estimated Parking costs']/td/span/b"
5  return cost_element
6 end

Tony: 那个常量10000是干什么的?

Alex:那是个超时值。驱动最多等待10秒钟,要么新页面成功载入,要么测试会失败。

Tony:现在我们来清理一下这些乱糟糟的代码。我建议同样把xpath提取到一个常量中。

Alex:首先,我们看一下是否能正确运行。我们来运行一下测试。

Alex和Tony启动了测试,看着它执行了停车场费用计算,最后在命令行中显示绿色,代表所有代码工作良好(见程序清单2-19)。

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

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   Then I will have to pay $ 12.00
    # step_definitions/Valet_steps.rb:6
7
8 1 scenario (1 passed)
9 2 steps (2 passed)
10 0m0.324s

Tony:很好,测试通过了。在我们做任何修改之前,首先提交一下代码,这样将来我们可以回滚我们所做的任何修改,以防万一嘛。

Alex:好主意。

Alex和Tony把他们的成果提交到版本控制系统。

Alex:现在我们回到你的建议。想法很好,但是我想先把这个函数分成两步。第一个函数用来点击“提交”按钮并等待页面载入。第二个函数会取到算好的费用并返回。我写给你看我是什么意思。

Alex从最初的parking_costs函数提出两个函数(见程序清单2-20)。

程序清单2-20 将两个步骤重构到各自函数后的检查代码

1  def parking_costs
2   calculate_parking_costs
3   get_parking_costs_from_page
4  end
5
6  def calculate_parking_costs
7   @page.click 'Submit'
8   @page.wait_for_page_to_load 10000
9  end
10
11  def get_parking_costs_from_page
12   @page.get_text "//tr[td/div[@class='SubHead'] = 'estimated
    Parking costs']/td/span/b"
13  end

Tony:现在我们来把定位费用元素的xpath路径移到常量中去。

Alex:既然要做这个,不如把“计算”按钮的名字也用一个有意义的变量名表示好了。

Tony: 现在,最后再跑一遍测试,然后我们就可以把代码提交到源代码库里了。

Tony和Alex看着测试运行通过了。他们将文件提交到了代码库。程序清单2-21展示了step_definitions/Valet_steps.rb的最终内容,程序清单2-22是lib/parkcalc.rb的最终内容。

程序清单2-21 代客泊车第一个测试步骤定义的最终版本

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 end
5
6 Then /^I will have to pay (.*)$/ do |price|
7  $parkcalc.parking_costs.should == price
8 end

程序清单2-22 第一个测试ParkCalcPage类的最终代码

1 class ParkCalcPage
 2
 3  @@lotIdentifier = 'ParkingLot'
 4  @@startingPrefix = 'Starting'
 5  @@leavingPrefix = 'Leaving'
 6  @@dateTemplate = "%sDate"
 7  @@timeTemplate = "%sTime"
 8  @@amPMRadioButtonTemplate = "//input[@name='%sTimeAMPM' and
    @value='%s']"
 9
10  @@calculateButtonIdentifier = 'Submit'
11  @@costElementLocation = "//tr[td/div[@class='SubHead']
    = 'estimated Parking costs']/td/span/b"
12
13  @@durationMap = {
14   '30 minutes' => ['05/04/2010', '12:00', 'AM', '05/04/2010'
    , '12:30', 'AM']
15  }
16
17  attr :page
18
19  def initialize(page_handle)
20   @page = page_handle
21   @page.open '/parkcalc'
22  end
23
24  def select(parking_lot)
25   @page.select @@lotIdentifier, parking_lot
26  end
27
28  def enter_parking_duration(duration)
29   startingDate, startingTime, startingTimeAMPM, leavingDate,
    leavingTime, leavingTimeAMPM = @@durationMap[duration]
30   fill_in_date_and_time_for @@startingPrefix, startingDate,
    startingTime, startingTimeAMPM
31   fill_in_date_and_time_for @@leavingPrefix, leavingDate,
    leavingTime, leavingTimeAMPM
32  end
33
34  def fill_in_date_and_time_for(formPrefix, date, time, ampm)
35   @page.type @@dateTemplate % formPrefix, date
36   @page.type @@timeTemplate % formPrefix, time
37   @page.click @@amPMRadioButtonTemplate % [ formPrefix,
    ampm ]
38  end
39
40  def parking_costs
41   calculate_parking_costs
42   get_parking_costs_from_page
43  end
44
45  def calculate_parking_costs
46   @page.click @@calculateButtonIdentifier
47   @page.wait_for_page_to_load 10000
48  end
49
50  def get_parking_costs_from_page
51   @page.get_text @@costElementLocation
52  end
53 end

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

相关文章
|
4月前
|
人工智能 数据可视化 测试技术
UAT测试排程工具深度解析:让验收测试不再失控,项目稳稳上线
在系统交付节奏加快的背景下,“测试节奏混乱”已成为项目延期的主因之一。UAT测试排程工具应运而生,帮助团队结构化拆解任务、清晰分配责任、实时掌控进度,打通需求、测试、开发三方协作闭环,提升测试效率与质量。本文还盘点了2025年热门UAT工具,助力团队选型落地,告别靠表格和群聊推进测试的低效方式,实现有节奏、有章法的测试管理。
|
数据采集 自然语言处理 数据库
深入体验阿里云通义灵码:测试与实例展示
阿里云通义灵码是一款强大的代码生成工具,支持自然语言描述需求,快速生成高质量代码。它在测试、代码质量和用户体验方面表现出色,能够高效地生成 Python 和 Java 等语言的代码,助力开发者提升开发效率和代码质量。无论是新手还是资深开发者,都能从中受益匪浅。
深入体验阿里云通义灵码:测试与实例展示
|
机器学习/深度学习 JSON 算法
实例分割笔记(一): 使用YOLOv5-Seg对图像进行分割检测完整版(从自定义数据集到测试验证的完整流程)
本文详细介绍了使用YOLOv5-Seg模型进行图像分割的完整流程,包括图像分割的基础知识、YOLOv5-Seg模型的特点、环境搭建、数据集准备、模型训练、验证、测试以及评价指标。通过实例代码,指导读者从自定义数据集开始,直至模型的测试验证,适合深度学习领域的研究者和开发者参考。
4702 3
实例分割笔记(一): 使用YOLOv5-Seg对图像进行分割检测完整版(从自定义数据集到测试验证的完整流程)
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
198 2
|
Java 测试技术 API
SpringBoot单元测试快速写法问题之创建 PorkInst 实例如何解决
SpringBoot单元测试快速写法问题之创建 PorkInst 实例如何解决
|
存储 测试技术 API
apifox实例应用-自动化测试用例for循环的使用
总结来说,通过在Apifox自动化测试用例中结合for循环的使用,我们可以有效地对接口进行批量测试,提升测试效率和覆盖率。同时,通过参数化测试数据的灵活应用,能够确保我们的接口在不同的输入条件下都能保持正确的行为。这种方法能够显著减少手动测试工作量,同时通过标准化的流程确保测试的一致性。
825 0
|
NoSQL 关系型数据库 MySQL
软件测试之【基于开源商城系统fecmall功能测试项目实例】
软件测试之【基于开源商城系统fecmall功能测试项目实例】
1196 0
软件测试之【基于开源商城系统fecmall功能测试项目实例】
|
测试技术 数据库 Python
在系统工程中,软件测试是一个至关重要的环节,它确保软件的质量、可靠性和性能。软件测试通常包括多个阶段,如单元测试、集成测试、系统测试和验收测试等。
在系统工程中,软件测试是一个至关重要的环节,它确保软件的质量、可靠性和性能。软件测试通常包括多个阶段,如单元测试、集成测试、系统测试和验收测试等。
|
DataWorks NoSQL 关系型数据库
DataWorks操作报错合集之在使用 DataWorks 进行 MongoDB 同步时遇到了连通性测试失败,实例配置和 MongoDB 白名单配置均正确,且同 VPC 下 MySQL 可以成功连接并同步,但 MongoDB 却无法完成同样的操作如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
279 1
|
存储 弹性计算 网络协议
【阿里云弹性计算】ECS实例性能测试报告:阿里云实例性能横向评测
【5月更文挑战第27天】阿里云ECS性能横向评测对比了经济型e系列、计算型c7a系列实例的CPU、内存、网络和存储性能。使用SPEC CPU 2017、Stream、iperf和fio工具进行测试。结果显示,计算型c7a系列在CPU和网络性能上突出,经济型e系列性价比高。所有实例内存性能良好,ESSD云盘提供出色存储性能。用户应根据业务需求选择合适实例。
484 0