第三篇 项 目 篇
归根结底,学习基础的目的就是为了应用,在实战的过程中可以学习到更加深入的知识。结合实际项目的学习能够更好地理解理论知识,深化对理论知识的认识。如同编程一样,刚开始项目很简单、很小,慢慢地随着学习的深入,我们就可以迭代项目代码,扩充完善项目功能。在不断地试错、迭代完善项目的同时,项目的轮廓会越来越清晰。
初学者项目不要贪多、贪大。先分析需求点,然后一点一点去思考怎么实现,解析分割需求时,最好做到功能模块的独立性,这样可以减少程序的耦合度。回到自动化测试的话题,低耦合也可以增强自动化框架的可扩展性。
从简单到复杂、逐步深入分析测试需求,实现我们的项目需求,到最后优化项目需求是一个逐步增强的过程。本篇章节如下。
第 8 章 项目实战
第 9 章 代码优化与项目重构
第 10 章 数据驱动测试
第 11 章 Page Object 设计模式
第 12 章 行为驱动测试
第 8 章 项目实战
本章通过项目实战的方式来让读者对自动化测试有个系统的认识。通过项目实战可以更快地将基础知识串联起来。
8.1 项目需求分析汇总
此部分内容是分析项目要覆盖的业务场景,以及要实现自动化的功能点。这也是真正开始写自动化测试代码之前的一个必要的步骤,在做自动化项目时需要进行前期调研,根据调研结果来确定项目相关的属性,比如适用自动化测试的业务范围,即那些功能适合自动化,确定好测试方法、测试策略等方式。
8.1.1 制定项目计划
制定一个有效的项目计划。项目计划的制定至关重要,它的好坏直接决定项目是否能成功。制定计划就是通盘考虑能预见的活动集合及异常风险等问题。做计划就是为了对这些异常项提早构思弥补方案等。
项目一定要有范围,把范围定义清楚是很重要的。假如项目的范围不清晰,那么最大的问题是,做到什么程度项目是成功的呢。范围不清晰也会带来另外一个问题即验证标准无法确定。因此,范围一定要明晰。如某次自动化测试的项目范围就是测试某网站的登录功能,所以对于网站的其他功能就不需要考虑在内了。
其次是对项目设定目标,对于这一点需要综合评估项目本身,如范围,涉及业务类型、大小、风险评估等。综合评估之后,设定一个较合理的目标。
项目目标设定完成后,就要规划一些活动来支持和完成既定目标。规划活动需要围绕项目目标,否则就没有意义。所以在项目中要避免无效的活动。为了项目目标,需要将这些无效或者低效的活动从项目规划中剔除。
项目计划需要练习。这种描述看起来比较诡异,但确实是笔者最切实的感受。为项目做计划,是没有标准答案的。同一个项目,不同的人去做,最终做出来的项目计划肯定会有不同之处。所以笔者才认为项目计划需要做练习的意思是,需要在不同的、多种多样的项目中做项目计划的练习。
项目执行需要遵循项目计划,如果计划没有被一项一项地落实,那么项目计划的制定只是走过场,没有实质性的作用。项目计划成熟的标志是,它要具有独立性和非主观性,也就是说,一个项目计划,即使让非制定者去执行,也可以顺利进行。
在本篇的项目实战中,项目计划制定如表 8-1 所示,出于篇幅限制,此项目计划书为精简版,只列出关键项,没有时间、人员等信息。读者在实际的项目中,请根据项目的实际情况来创建属于自己的项目计划。项目计划没有固定的格式和内容要求,一个项目实例如表 8.1 所示。
表 8.1
8.1.2 制定测试用例
自动化测试也是需要编写测试用例的,这是规范和追踪测试活动的一个必不可少的环节。切忌在没有测试用例的情况下直接写脚本,或者直接拿手工测试的测试用例作为自动化测试用例。在编写自动化测试用例时,通常要做到以下两点:
· 需要简明扼要,便于理解和易于执行。
· 就测试用例的总体性而言,场景要覆盖全面,比如正反例等。
请参考如下测试用例实例,如表 8.2 所示。
表 8.2
我们以携程网购买火车票为例,这里的需求分析主要是业务场景的覆盖和对每个页面上的关键元素的分析。需求分析的主要流程如图 8.1 所示。
图 8.1
以上是对项目需求的一个简单的分析,通过以上流程图的形式更能清晰地勾勒出项目的需求点。让测试的参与者从全局的角度出发有一个清晰的认识。
8.2 业务场景覆盖与分拆
针对以上的需求分析步骤,业务场景覆盖与分拆是项目进行的前提,这里会做详细的说明。
具体场景是在「携程网」上预订火车票。为了使测试简单,这里只演示在非登录状态下的车票预订场景,并且由于篇幅的限制,项目篇只涉及测试用例中的正向用例(测试用例 ID 为 booking_ticket_01)。
被测试功能主要涉及的页面描述如下:
「火车票查询」页面如图 8.2 所示。
页面 URL 为「https://trains.ctrip.com/TrainBooking/SearchTrain.aspx」。
输入「出发城市」「到达城市」和「出发时间」等必要的信息后,即跳转到「车次列表」页面,如图 8.3 所示。此页面显示的是 4 月 1 号上海到杭州所有的车次列表。
图 8.2
图 8.3
在「车次列表」页面选择要订购的车票,单击「预订」按钮后页面跳转到「携程账号登录」页面,如图 8.4 所示。
图 8.4
在「携程账号登录」页面,在页面的最下方单击「不登录,直接预订 >」按钮,跳转到「订单信息」页面,如图 8.5 所示。
图 8.5
以上是对项目中涉及的页面的粗略统计和认识,总共涉及 3 个页面,具体如下:
· 火车票查询页面。
· 车次列表页面。
· 订单信息页面。
8.2.1 逐个页面元素分析
对项目中用到的元素进行分析。元素定位的方法需要结合元素自身的实际,不一定要用某一种的定位方式。一般的元素定位方式的选择有一定的优先级,比如优先选择 id 定位方式,其次是 name、classname、css、xpath 等方式。对火车票查询页面进行元素分析,如图 8.6 所示。
1.出发城市【输入框】
此元素的细节如图 8.6 中的高亮部分所示,通过 id 定位的方式来定义元素,值为「notice01」。
图 8.6
2.到达城市【输入框】
此元素的细节如图 8.7 中的高亮所示,通过 id 定位的方式来定义元素,值为「notice08」。
3.出发时间【日期输入框】
此元素的细节如图 8.8 中的高亮部分显示,可以通过 id 定位的方式来定义元素,值为「dateObj」。在分析页面元素时,读者还可以发现一个细节,即「出发时间」元素有一个属性 readonly,并且其值为「readonly」。
注意:这个字段在 WebDriver 中是不允许直接赋值的。具体内容本章后续会介绍。
图 8.7
图 8.8
4.开始搜索【按钮】
「开始搜索」是一个按钮,用于提交搜索表单。该元素通过 id 定位的方式来定义元素,值为「searchbtn」,如图 8.9 所示。
图 8.9
对车次列表页面相关的页面元素进行定位和分析,如图 8.10 所示。
图 8.10
5.预订【超链接】
预订按钮是一个超链接,用于提交火车票订单,如图 8.11 所示。通过分析元素的 HTML 代码,采用 CSS 方式来定位。定位代码为「driver.find_element_by_css_selector(「#tbody-01- K18050> div.railway_list > div.w6 > div:nth-child(1)> a」)」。
图 8.11
在「携程账号登录」页面,需要对页面元素「不登录,直接预订 >」进行定位和分析,如图 8.12 所示。
图 8.12
6.元素「不登录,直接预订 >」【超链接】
这是一个超链接,用于实现不用登录即可订票的目的。通过分析元素细节,发现可以用 id 来定位元素,定位代码为「driver.find_element_by_id(「btn_nologin」)」。
在订单信息页面,需要对页面元素「乘客信息」进行定位和分析,如图 8.13 所示。通过分析元素的细节可以发现,CSS 方式是比较适合此元素定位的。定位语句为「driver.find_element_by_css_selector(「#pasglistdiv > div > ul > li:nth-child(2)> input」)」。
图 8.13
8.2.2 分层创建脚本
根据火车票查询页面元素定位的情况,先创建 Python 脚本文件 search_tickets.py。在讲解详细的脚本之前,需要了解一下相关的基础知识点,便于更好地理解代码。
1.Python 中导入模块语句
Import 语句是用来导入模块的,原则上它可以出现在程序中的任何位置。举例如下:import math 作用是导入 math 模块。一次性导入多个模块可用如下写法:「import module1,module2,module3…」;导入一个模块中的一个方法,写法为「from math import floor」;导入一个模块中的多个方法,类似多个模块的方式,可以用逗号分隔。总体来说,import 语句导入模块时最好按照如下顺序,这样的代码逻辑比较清晰和规范:
(1)Python 标准库模块即 Python 自带库。
(2)Python 第三方模块。
(3)自定义模块。
2.函数
函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,目的是为了提高程序代码的复用性和可读性。Python 函数又具有一些独特的优势(可以更灵活地定义函数等),此外 Python 自身也内置了很多有用的函数,开发人员可以直接调用。
函数有六大要点,分别是:
(1)def。
(2)函数名。
(3)函数体。
(4)参数。
(5)返回值。
(6)两个英文符号,小括号(),里面是参数的定义内容和冒号。
Python 函数有五大要素:def、函数名、函数体、参数、返回值。
如图 8.14 所示是一个函数的样例,实现了求和的功能,其中 def sum(a,b)部分声明函数名「sum」和参数部分「a,b」,后面紧接一个冒号「:」可以理解为声明函数的结束标志;「return a+b」是函数体,Python 语法规定函数体与函数声明部分相比是缩进的,没有缩进程序会报错。如果函数体结束,则缩进结束即可(缩进结束代表函数体结束)。示例中的函数体只有一行。
图 8.14
返回值。函数可以有返回值也可以没有返回值,如图 8.14 中的函数是有返回值的情况,如下函数代码是无返回值的情况:
Python 的函数参数模式比较复杂。以下用实例来演示各种参数模式。
如下函数定义是属于位置参数类型的函数,位置参数也是我们熟悉的形参,其中「x」就是一个位置参数:
如下函数定义用到了默认参数,其中「b」是默认参数,默认值为「2」。在函数调用时如果该参数没有传入相应的值,就可以启动默认值。如调用 1: testFunc2(4),其结果是返回「6」;调用 2: testFunc2(4,8),其结果是返回「12」。
注意事项:设置参数顺序时,必须是必选参数在前,默认参数在后,否则 Python 解释器会报错;默认参数必须指向不可变对象。相关代码如下:
可变参数,顾名思义在函数定义过程中,参数的个数是不固定的,可以是 1 个、2 个,还可以是 0 个,等等。比如,在将一个 list 对象作为参数而 list 对象的数据又不确定的情况下,可以使用可变参数的模式简化函数的定义。如下源码的功能是计算列表中元素的平方和,而列表元素作为参数是不确定的、可变的。
函数调用 1:testFunc3([1,2,3]),用来计算列表[1,2,3]的所有元素的平方和,结果是「14」。
函数调用 2:testFunc3([2,3,4,5]),用来计算列表[2,3,4,5]的所有元素的平方和,结果是「54」。
关键字参数形式有点类似可变参数,其实两者是有差别的。可变参数允许传入 0 或者任意多个参数,而这些参数在函数调用时自动组装成一个元组。关键字参数允许传入 0 个或任意个含参数名的参数,而这些关键字参数在函数内部自动组装成一个字典类型。演示实例源码如下,通过几种函数调用来直观地了解关键字参数的特性。
关键字参数调用 1,testFunc4(‘29’),控制台报错,提示缺少一个位置参数『name』。结果如图 8.15 所示。
图 8.15
关键字参数调用 2,执行结果如图 8.16 所示,此时执行没有错误产生且『other』字典类型值为空。
图 8.16
关键字参数调用 3,执行结果如图 8.17 所示,other 字典类型有一个键值对{『city』:『Shanghai』}。
图 8.17
关键字参数调用 4,执行结果如图 8.18 时 other 字典类型有两个键值对{『city』: 『Shanghai』,『age』:『24』}。至此关键字参数调用的模式和特性通过 4 个实例演示已经比较直观地展现出来,比较灵活。在使用的时候,要注意区别不同参数模式的异同。
图 8.18
关键字参数允许函数调用者传入任意不受限制的关键字参数,如果要限制关键字参数的名字,可以用命名关键字参数模式进行限制。示例演示源码如下,函数 testFunc5 关键字参数只接收「city」和「age」。
命名关键字参数调用 1,执行结果如图 8.19 所示,关键字参数包含了「city」和「age」,而输出结果中只打印 value,没有打印 key。
命名关键字参数调用 2,执行结果如图 8.20 所示,关键字参数有 3 个,分别是「city」「age」和「job」。与函数定义有冲突(只允许「city」和「age」)。
图 8.19
图 8.20
命名关键字参数调用 3,关键字参数有 1 个,是「city」。与函数定义还是有冲突,控制台提示少了「age」关键字参数,执行结果如图 8.21 所示。
图 8.21
初始脚本如下:
#chromedriver.exe放在D:Python38 from selenium import webdriver import time driver = webdriver.Chrome() driver.maximize_window() #以下变量定义搜索火车票的出发站和到达站 from_station = "上海" to_station = "杭州" driver.get('http://trains.ctrip.com/TrainBooking/SearchTrain.aspx') driver.find_element_by_id('departCityName').send_keys("from_station") driver.find_element_by_id("arriveCityName").send_keys("to_station") #以下为定位"车次搜索"按钮 driver.find_element_by_class_name("searchbtn").click()
执行以上脚本,结果如图 8.22 所示,发现脚本是有问题的,问题出在出发时间的选择上,无法正确选择「2019-04-12」,无法对日期直接赋值。
图 8.22
以上问题的解决思路是,用 Javascript 来更改元素的属性,以达到测试的目的。在 WebDriver 中提供了可以执行 JavaScript 代码的接口或者功能。JavaScript 可以完成一些 WebDriver 本身所不能完成的功能,在 WebDriver 中可以用函数「execute_script」来执行 JavaScript 代码,相关 Python 代码如下:
更新后的火车票查询脚本如下:
#chromedriver.exe放在D:Python38 from selenium import webdriver import time driver = webdriver.Chrome() driver.maximize_window() #以下变量定义搜索火车票的出发站和到达站 from_station = "上海" to_station = "杭州" driver.get('http://trains.ctrip.com/TrainBooking/SearchTrain.aspx') driver.find_element_by_id('departCityName').send_keys(from_station) driver.find_element_by_id("arriveCityName").send_keys(to_station) #移除出发时间的'readonly'属性 js="document.getElementById('departDate').removeAttribute('readonly')" driver.execute_script(js) #定义搜索车次日期 # driver.find_element_by_id('departDate').send_keys("2020-12-25") #以下为定位"车次搜索"按钮 driver.find_element_by_class_name("searchbtn").click()
修改之后的代码执行结果如图 8.23 所示,页面输入有异常,原因是出发时间有默认值,需要继续优化代码。
图 8.23
关于出发时间的优化有两处:一是在输入出发时间前,先清空其内容,再输入;二是修改调整代码不用硬编码,如实现出发时间为第二天的日期。若要判断日期和处理日期等信息需要导入 datetime 功能模块,模块导入语句为「from datetime import datetime,date,timedelta」。优化后的代码如下:
#chromedriver.exe放在D:Python38 from datetime import datetime,date,timedelta from selenium import webdriver import time #以下为定义函数部分,其目的是返回今天后的第n天的日期,格式为"2019-04-03" def date_n(n): return str((date.today() + timedelta(days = +int(n))).strftime("%Y-%m-%d")) #以下变量定义搜索火车票的出发站和到达站 from_station = "上海" to_station = "杭州" #以下为tomorrow变量 tomorrow = date_n(1) print(tomorrow) driver = webdriver.Chrome() driver.maximize_window() driver.get('http://trains.ctrip.com/TrainBooking/SearchTrain.aspx') driver.find_element_by_id('departCityName').send_keys(from_station) driver.find_element_by_id("arriveCityName").send_keys(to_station) #移除出发时间的'readonly'属性 js="document.getElementById('departDate').removeAttribute('readonly')" driver.execute_script(js) time.sleep(2) #清楚出发时间的默认内容 driver.find_element_by_id('departDate').clear() time.sleep(2) #以下为定义搜索车次日期 driver.find_element_by_id('departDate').send_keys(tomorrow) # driver.find_element_by_id('departDate').send_keys("2020-12-25") #以下为定位"车次搜索"按钮 driver.find_element_by_class_name("searchbtn").click()
以上代码执行完毕后,控制台报错,因弹出窗口没有消除,如图 8.24 所示。
图 8.24
由于弹出窗的问题导致「搜索」按钮定位失败,代码的执行结果如下所示:
要解决此问题,只需用 ActionChains 功能,用鼠标左键单击页面的空白处。针对以上问题,优化后的代码如下:
以上代码执行后,结果如图 8.25 所示。证明火车票查询页面的脚本已经成功地执行完毕。
图 8.25
根据上一节的关于车次列表的元素定位的情况,增加车次预订功能,代码如下:
执行脚本后,弹出了「携程账号登录」页面,如图 8.26 所示。由此可见脚本执行是成功的,并且业务逻辑是正确的。
图 8.26
进一步完善测试脚本加上处理「携程账号登录」页面的功能。单击「不登录,直接预订 >」进入下一步(页面),代码如下:
以上代码的执行结果如图 8.27 所示,之后跳转到「订单信息」页面。
图 8.27
最后,在订单信息页面上实现输入乘客姓名的功能,代码如下:
#chromedriver.exe放在D:Python38 ''' 此页面的功能是测试火车票的页面元素 ''' from datetime import datetime,date,timedelta from selenium import webdriver from selenium.webdriver.common.action_chains import ActionChains import time #以下为定义函数部分,其目的是返回今天后的第n天的日期,格式为"2019-04-03" def date_n(n): return str((date.today() + timedelta(days = int(n))).strftime("%Y-%m-%d")) #以下变量定义搜索火车票的出发站和到达站 from_station = "上海" to_station = "杭州" #以下为tomorrow变量 tomorrow = date_n(1) print(tomorrow) driver = webdriver.Chrome() driver.maximize_window() driver.get('http://trains.ctrip.com/TrainBooking/SearchTrain.aspx') driver.find_element_by_id('departCityName').send_keys(from_station) time.sleep(2) driver.find_element_by_id("arriveCityName").send_keys(to_station) #移除出发时间的'readonly'属性 js="document.getElementById('departDate').removeAttribute('readonly')" driver.execute_script(js) time.sleep(2) #清楚出发时间的默认内容 driver.find_element_by_id('departDate').clear() time.sleep(2) #以下为定义搜索车次日期 driver.find_element_by_id('departDate').send_keys(tomorrow) # driver.find_element_by_id('departDate').send_keys("2020-12-25") # 以下步骤是为了解决日期控件弹出窗在输入日期后无法消失的问题 # 原理是为了让鼠标左键单击页面空白处 ActionChains(driver).move_by_offset(100,100).click().perform() #以下为定位并单击"车次搜索"按钮 driver.find_element_by_class_name("searchbtn").click() #在页面跳转时最好加一些时间等待的步骤,以免元素定位出现异常 time.sleep(2) #通过在K1805车次的硬座区域单机"预定"按钮来预定车票 driver.find_element_by_css_selector("body > div:nth-child(33) > div > div.lisBox " "> div.List-Box > div > div:nth-child(1) > div.w6 > div:nth-child(1) > a").click() time.sleep(3) #添加第一位成人信息 #以下为订票人 dingpiao_adult="康冕峰" adult_id=110111 phone_number=156 driver.find_element_by_css_selector("#inputPassengerVue > div.pasg-add > ul > li:nth-child(2) > input").send_keys(dingpiao_adult) driver.find_element_by_css_selector("#inputPassengerVue > div.pasg-add > ul > li:nth-child(3) > input").send_keys(adult_id) driver.find_element_by_css_selector("#inputPassengerVue > div.pasg-add > ul > li:nth-child(6) > input").send_keys(phone_number) driver.find_element_by_css_selector("#contact-mobile").send_keys(phone_number)
以上脚本执行之后,结果如图 8.28 所示,乘客的信息已经输入成功,说明以上的脚本代码是完整而正确的。至此,项目实战的初始脚本代码已经完成。
图 8.28
8.3 项目代码总结
前面详细分析了车次查询、车次列表、订单详情页面的所有元素,用线性代码实现了整个业务流程,完整代码如下: