第 9 章 代码优化与项目重构
9.1 项目重构
本章将继续以携程网订购火车票为例,在原先的代码上做进一步优化和重构,有利于加深对项目重构的认识。项目重构通常利用抽象的方法重新组织代码,进而有效地提高代码的重用性和可维护性。
9.1.1 重构——元素定位方法优化
元素的定位方法可能会被多处代码调用,此案例中就涉及多个页面,如火车查询页面、车次列表页面等。每张页面在进行元素定位时又需要用到元素定位方法,所以对元素定位方法进行重构再封装是有必要的,也是有价值的。承接第 8 章之后产生的代码如下:
#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)
接下来,将重点介绍一下如何通过重构页面元素的方式对以上脚本进行再次优化。优化的目的主要有两点,一是可以减少代码量并且有效提高代码复用率;二是可以提高代码的可读性。代码重构可以通过定义函数来实现。按照之前的论述,函数的一个很重要的作用就是提高代码的重用性。
第一个函数根据元素 id 属性值来返回元素定位语句。其中「id」为函数名,「element」为函数参数。在函数体中返回函数定义语句,其中 id 属性值为函数传入的参数「element」。
第二个函数根据元素 CSS 属性值来返回元素定位语句。
第三个函数封装 JavaScript 脚本代码。请读者注意如下函数的写法,在函数体中需要将 id 属性值「element」用单引号标注,因为外围是双引号。
代码重构之后,测试脚本代码如下。其中有一点需要注意的是,如果将定义函数的代码和测试代码放在同一个 Python 文件中,需要将函数定义的部分放到测试代码的前面,代码如下:
#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")) def id(element): return driver.find_element_by_id(element) def css(element): return driver.find_elements_by_css_selector(element) def js(j_s): driver.execute_script(j_s) def class_name(element): return driver.find_element_by_class_name(element) #以下变量定义搜索火车票的出发站和到达站 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') id('departCityName').send_keys(from_station) time.sleep(2) id("arriveCityName").send_keys(to_station) #移除出发时间的'readonly'属性 j_s="document.getElementById('departDate').removeAttribute('readonly')" js(j_s) time.sleep(2) #清楚出发时间的默认内容 id('departDate').clear() time.sleep(2) #以下为定义搜索车次日期 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() #以下为定位并单击"车次搜索"按钮 class_name("searchbtn").click() #在页面跳转时最好加一些时间等待的步骤,以免元素定位出现异常 time.sleep(2) #通过在K1805车次的硬座区域单机"预定"按钮来预定车票 css("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)
9.1.2 车次信息选择优化
上一节中对脚本做了进一步优化,通常是可以运行成功的,但是偶尔会有例外的情况。经过分析,造成脚本运行不稳定的代码行如下所示。在车次列表中,「K1805」位于车次列表的第一位,如果车次顺序有变,脚本运行就会失败。因此,这种类型的代码会降低脚本的健壮性。
代码的不健壮性偶尔会导致如下所示的错误:
可以看出,导致出错的代码是按照 CSS 定位方式来定位元素的,这里需要改变一下思路。前面已经介绍了很多种元素定位的方式,此时就需要活学活用了。该问题的解决方案有多种,这里我们选用其中一种较为便捷的方法,就是使用 XPath 结合模糊查询的方式来定位该元素。关于模糊查询,前面已经讲解过,这里就不再赘述了。上面的定位语句可以改写成:
以上代码用到了自定义的 xpath 函数,其定义语句如下:
优化之后的代码如下:
#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")) def id(element): return driver.find_element_by_id(element) def css(element): return driver.find_element_by_css_selector(element) def js(j_s): driver.execute_script(j_s) def class_name(element): return driver.find_element_by_class_name(element) def xpath(element): return driver.find_element_by_xpath(element) #以下变量定义搜索火车票的出发站和到达站 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') id('departCityName').send_keys(from_station) time.sleep(2) id("arriveCityName").send_keys(to_station) #移除出发时间的'readonly'属性 j_s="document.getElementById('departDate').removeAttribute('readonly')" js(j_s) time.sleep(2) #清楚出发时间的默认内容 id('departDate').clear() time.sleep(2) #以下为定义搜索车次日期 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() #以下为定位并单击"车次搜索"按钮 class_name("searchbtn").click() #在页面跳转时最好加一些时间等待的步骤,以免元素定位出现异常 time.sleep(2) #通过在K1805车次的硬座区域单机"预定"按钮来预定车票 # css("body > div:nth-child(33) > div > div.lisBox " # "> div.List-Box > div > div:nth-child(1) > div.w6 > div:nth-child(1) > a").click() #通过XPath方式定位元素 xpath("/html/body/div[7]/div/div[5]/div[3]/div/div[1]/div[6]/div[1]/a").click() time.sleep(3) #添加第一位成人信息 dingpiao_adult="康冕峰" adult_id=110111 phone_number=156 #以下为订票人各项填入的信息 css("#inputPassengerVue > div.pasg-add > ul > li:nth-child(2) > input").send_keys(dingpiao_adult) css("#inputPassengerVue > div.pasg-add > ul > li:nth-child(3) > input").send_keys(adult_id) css("#inputPassengerVue > div.pasg-add > ul > li:nth-child(6) > input").send_keys(phone_number) css("#contact-mobile").send_keys(phone_number)
9.1.3 重构——代码分层优化
在这一节中,将继续优化以上代码。通过观察发现,上面的代码将函数和其他测试代码放在同一个文件中。随着自动化测试的深入,测试的内容和范围会逐步增加,这样的编码方式,不利于提高代码的可扩展性和可维护性。
为了更好地理解代码分层的理念,笔者将根据同样的项目逐步进行深入挖掘和优化。如图 9.1 所示为初步代码分层后的代码结构图。其中「booking_tickets.py」为测试代码文件;文件「functions.py」主要存放常用的基础方法等。
图 9.1
其中,测试代码文件的代码如下:
基础常用方法代码如下:
9.1.4 重构——三层架构
本节将继续对自动化代码进行重构,以上对代码的重构依旧有弱点,不够清晰明了。继续将分层优化的思维进行到底,这里需要对之前的代码结构做一些调整,便于自动化测试项目的管理和维护,也能减少项目的维护成本等。
如图 9.2 所示给出的是项目重构的三层架构示意图。
图 9.2
基于以上原则,将 Selenium、WebDriver 相关的配置加入 functions.py 基础代码文件中,便于底层调用。
functions.py 代码如下:
my_functions
''' 代码重构的定位函数等 ''' from datetime import datetime,date,timedelta from selenium import webdriver #以下为driver设置获得1个浏览器对象 driver = webdriver.Chrome() ''' 函数 return_driver()的功能是返回driver对象 ''' def return_driver(): return driver ''' 函数 open_base_site(url)的功能是打开网站web页面 ''' def open_site(url): driver.get(url) #以下为定义函数部分,其目的是返回今天后的第n天的日期,格式为"2019-04-03" ''' 函数date_n(n)将返回n天后的日期 ''' def date_n(n): return str((date.today() + timedelta(days = int(n))).strftime("%Y-%m-%d")) """ 下面的函数是,根据8种selenium定位方法,做二次封装 """ def id(element): return driver.find_element_by_id(element) def css(element): return driver.find_element_by_css_selector(element) def class_name(element): return driver.find_element_by_class_name(element) def xpath(element): return driver.find_element_by_xpath(element) """ 函数js通过selenium来执行JavaScript语句 """ def js(j_s): driver.execute_script(j_s) if __name__ == '__main__': print(__name__)
对于业务代码层,可以将之前的测试代码根据功能模块等进行拆分,比如在此项目中,可以将搜索车次的功能单独抽取成一个文件,如 search_tickets.py,它属于业务代码层。
search_tickets.py 文件代码如下:
#chromedriver.exe放在D:Python38 ''' 此页面的功能是测试火车票查询的页面元素 ''' import sys from selenium.webdriver.common.action_chains import ActionChains sys.path.append(r'C:UsersCDVPycharmProjects est_seleniumday09') from my_functions import date_n,id,css,xpath, class_name,js,return_driver,open_site import time ''' 函数名:search_tickets 参数: from_station:出发站 to_station:到达站 n:是一个数字,如1表示选择明日的车票 ''' def search_tickets(from_station,to_station,n): driver =return_driver() open_site('http://trains.ctrip.com/TrainBooking/SearchTrain.aspx') from_station =from_station to_station=to_station tomorrow = date_n(n) #以下为定位出发城市和到达城市的页面元素,设置其值为以上定义值 id('departCityName').send_keys(from_station) # 在页面跳转时最好加一些时间等待的步骤,以免元素定位出现异常 time.sleep(1) id("arriveCityName").send_keys(to_station) # 在页面跳转时最好加一些时间等待的步骤,以免元素定位出现异常 time.sleep(1) # 移除出发时间的'readonly'属性 j_s = "document.getElementById('departDate').removeAttribute('readonly')" js(j_s) # 清楚出发时间的默认内容 id('departDate').clear() time.sleep(1) # 以下为定义搜索车次日期 id('departDate').send_keys(tomorrow) # 以下步骤是为了解决日期控件弹出窗在输入日期后无法消失的问题 # 原理是为了让鼠标左键单击页面空白处 ActionChains(driver).move_by_offset(100, 100).click().perform() # 以下为定位并单击"车次搜索"按钮 class_name("searchbtn").click() # 在页面跳转时最好加一些时间等待的步骤,以免元素定位出现异常 time.sleep(2) # 通过在K1805车次的硬座区域单机"预定"按钮来预定车票 # css("body > div:nth-child(33) > div > div.lisBox " # "> div.List-Box > div > div:nth-child(1) > div.w6 > div:nth-child(1) > a").click() # 通过XPath方式定位元素 xpath("/html/body/div[7]/div/div[5]/div[3]/div/div[1]/div[6]/div[1]/a").click() # 增加窗口最大化的操作是为了解决脚本偶尔不稳定的问题 driver.maximize_window()
而最终的测试代码文件 test_booking_tickets.py 如下:
#chromedriver.exe放在D:Python38 ''' 此页面的功能是测试火车票查询的页面元素 ''' import time import sys sys.path.append(r'C:UsersCDVPycharmProjects est_seleniumday09') from my_functions import css,xpath,js from search_tickets import search_tickets #以下搜索火车票列表 search_tickets("上海","杭州",1) #不登录携程系统订票 time.sleep(2) #添加第一位成人信息 dingpiao_adult="康冕峰" adult_id=110111 phone_number=156 #以下为订票人各项填入的信息 css("#inputPassengerVue > div.pasg-add > ul > li:nth-child(2) > input").send_keys(dingpiao_adult) css("#inputPassengerVue > div.pasg-add > ul > li:nth-child(3) > input").send_keys(adult_id) css("#inputPassengerVue > div.pasg-add > ul > li:nth-child(6) > input").send_keys(phone_number) css("#contact-mobile").send_keys(phone_number)
根据以往的经验,代码重构和提取是一个持续的过程,代码需要持续迭代。一般来说,UI 自动化项目是基于比较稳定的版本进行的。在提取代码时,需要用到函数来管理代码,尽可能地避免硬编码,适当地设置变量,以提高函数的灵活性,如上面例子中的 search_tickets 函数。
借助以上方法对代码实现重构之后,三层架构——基础代码层、业务代码层和测试代码层已经具备。测试代码越来越简洁,也越来越清晰,这也是代码重构和优化的最终目的。提高代码的可读性、重用性和易扩展性对自动化项目的实施是非常有帮助的。在自动化测试初期更需要好好规划代码结构和思路,有些自动化测试项目的失败,很大一部分原因就是由于前期的代码结构不合理,导致后期维护非常困难,重构的代价越来越大。
9.2 代码优化
笔者想分享关于代码优化的一些心得。首先需要想明白代码优化的目的是什么。如果一次代码优化不能解决一些项目的问题或者困惑,那么这次代码优化活动的意义就不大,可以考虑不做。
代码优化一般包含很多方面。首先,可以考虑框架代码的优化,比如更改底层调用,甚至代码;其次,可以考虑厘清项目结构、优化结构组成等,便于后期维护和推广;然后,可以考虑具体的使用,如可以考虑项目内部提出的一些代码标准、文档标准、运维管理标准等。这些都是从广义上理解的代码优化行为。
9.2.1 重构——项目异常处理
在自动化测试过程中,遇到异常是时有发生的,为了使测试代码更加健壮,需要在自动化项目中去处理这些异常。如何处理异常呢?首先需要搞清楚异常产生的原因,然后对这些异常进行处理。
接下来,将通过具体的例子来说明异常处理的重要性,以及处理这些异常的常用方法。
示例代码如下:
当代码执行到第三行时,由于除数为 0,所以代码会报错,具体错误如图 9.3 所示。
图 9.3
如何处理和管理这些异常呢?可以利用 Python 的 try 语句来捕捉异常,代码改写如下。
修改后,代码执行结果的可读性就比较好,而且代码执行完成后,会打印出便于识别的错误信息,比如代码中定义的「错误,除数为零」。
接下来,以之前的携程项目为例来说明异常处理的重要性,代码如下:
如果我们故意把「出发城市」元素的 id 属性值「notice01」改写成「notice0」,就会导致异常发生,即元素无法定位。具体结果如下:
下面截取本章项目中的部分代码来对 Selenium 中的异常进行练习。可以将上面的项目代码改写如下,让测试程序捕捉到异常时打印出信息「element not found」。这样在测试框架中,信息输出内容看起来就会比较简洁。
异常的处理和管理在组建自动化测试的过程中是非常重要的,通常在搭建自动化测试框架时就需要去考虑。测试人员要根据项目自身的特点和需要去重新定义异常,便于项目内部交流和自动化测试的执行。
Selenium 中常见的异常有 9 种(如表 9.1 所示),其中比较常用的是「NoSuchElementException」。
表 9.1
9.2.2 重构——智能等待
在实际的项目中,代码在执行定位页面元素的过程中有些是需要等待时间的,但是如果在所有定位元素的操作之前都加上等待时间就比较麻烦,并且不易维护。此时可以考虑智能等待,方法很简单,可以在代码前面加上全局的智能等待时间,比如「driver.implicitly_wait(10)」。
何为智能,这里需要解释一下,比如在代码中,设定时间为 10 秒,如果元素定位花了 2 秒,那么这个页面的等待时间就是 2 秒,而不是设置的 10 秒。如果 10 秒内还没有定位到元素就会报错,元素定位失败。示例代码如下所示: