同步点 让测试脚本更稳定—全栈软件测试自动化(6)

发表于:2020-7-01 10:30

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:赵旭斌 余杰    来源:51Testing软件测试网原创

  2.2  同步点—让测试脚本更稳定
  2.2.1  同步点的重要性
  同步点在自动化测试的过程中扮演着重要的角色。凡是阅读过《精通QTP—自动化测试技术领航》的读者一定对同步点这个概念都不陌生,其主要作用是在脚本中特定的测试步骤之前智能化插入停顿时间或者等待时间,为什么需要这样做呢?这里简单举一个例子,假设现在有这样一个测试用例,其测试步骤和预期结果如表2-2所示。
  表2-2 一个测试用例的测试步骤和预期结果
  这是一个很简单的手动测试用例,但是如果作为自动化测试用例,它就没有你想象中那么简单了。因为自动化测试脚本时刻需要“同步点”的帮助,如果不设置同步点,你就会发现即使再简单的测试用例也会让我们寸步难行。想象一下,当测试脚本执行到打开URL这一步后,浏览器会有一个加载的动作。无论网速有多快,浏览器都会有一个等待页面加载的时间。因为当你在浏览器的地址栏中输入URL时,其实这是在向对应的URL发送一个请求,接着浏览器需要等待服务器返回内容后才能将其内容展示到浏览器上,并形成可读的网页内容,所以这个过程中势必会有一个等待的时间。那么问题就来了,当测试脚本执行完了跳转URL后,测试脚本本身是不会有任何等待的,它会继续执行后续脚本中的步骤。换句话说,当页面还在加载时,测试脚本就已经执行到了输入用户名这个步骤,而此时当前页面还没有出现用户名这个元素,因此在没有同步点的帮助下,一个异常会被抛出—没有找到对应的对象。
  这时,很多读者会说:“在测试脚本中加一个等待时间不就可以解决了吗?比如加上等待5s时间。”是的,这的确是一种解决方法,也许加了5s,这个脚本的确可以顺利通过。但不推荐这样做,这是一种极不稳定又浪费时间的做法。首先,如果在网络环境较好的情况下在0.5s内加载完毕,那么脚本就要额外等待4.5s时间,这会浪费很多时间。一个脚本可能远远不止一个URL跳转,一个URL浪费4.5s,10个URL呢?另一种情况下,如果公司的某个同事正在进行下载,网络情况会变得比较糟糕,URL跳转过程基本上就超过5s了,那样测试脚本运行还会失败。正确的做法应该是,通过同步点智能地等待“最佳的”时间,即一旦页面加载完毕就结束等待。实际情况下,Selenium会自动等待页面加载(AJAX异步加载除外)完成才会去查找页面上的控件,测试脚本本身并不需要做任何设置。而对于类似AJAX这样异步加载的网页来说,Selenium在默认情况下就无能为力了,此时就需要在脚本中设置正确的同步点来解决问题。下面介绍智能全局等待和私人订制等待。
  2.2.2  智能全局等待
  其实从名字上看,读者应该就能略知一二了。所谓智能,即以非固定的方式智能地等待某一个控件是否存在,全局的意思就是设置一次就能对所有对象查找生效。言下之意就是,当一个脚本设置了智能全局等待后,脚本会自动对所有对象的查找过程进行一段时间的智能等待。一旦找到了对象,就停止等待;如果在指定时间里还没有找到对象,那么会抛出异常。设置方法如下。
 driver.implicitly_wait(10)
  对于智能全局等待来说,设置方法很简单,程序中的driver对象提供了一个方法—implicitly_wait。在图2.13中,从源代码的注释可以了解到,implicitly_wait方法在整个session里只需要设置一次即可生效,无须重复设置;此方法的参数只有一个,即time_to_wait,单位为秒,方法的参数若是10就表示10s,也就是如果在10s还找不到对象就抛出异常。如果将implicitly_wait方法放入脚本中,通常会把此方法放在在浏览器中创建的session对象之后,代码如下。
  图2.13  implicitly_wait的源代码
   from selenium import webdriver
  from selenium.webdriver.common.by import By
  driver = webdriver.Firefox()
  driver.get("Mercury Tours登录页面")
  driver.implicitly_wait(20)
  username_textbox = driver.find_element(By.NAME, "UserName")
  password_textbox = driver.find_element(By.NAME, "password")
  login_button = driver.find_element(By.NAME, "login")
  username_textbox.send_keys("mercury")
  password_textbox.send_keys("mercury")
  login_button.click()
  在上述脚本中,driver.implicitly_wait(20)就是设置智能全局等待时间的语句,此处设置的时间是20s。需要注意的是,如果没有这行代码,其默认的智能全局等待时间为0s。另外,请读者想一下,如果没有这行代码,以上脚本会不会报错呢?
  其实,在这个例子中,即使没有这行代码,脚本也不会报错,在前面讲到 Selenium的脚本中,即使没有做任何设置,运行时也会自动等待页面跳转完成。这个飞机订票系统并没有采用AJAX异步的方式加载网页内容,因此,当Selenium等待页面加载完时,所有的控件就已经完全地显示在页面上了,也就不会出现找不到对象的问题了。所以,在上面的代码片段中,不管有没有这行代码,脚本都不会报错,但是为了安全起见,建议在测试项目中都加上这段代码,这样无论对应的项目是否采用AJAX,测试脚本都不会因为同步点的问题而出错。
  2.2.3  私人订制等待
  私人订制等待可以对某个指定的条件进行等待,是一种更高级的同步用法。
  设置方法如下。
 WebDriverWait(driver, <timeout>).until(<condition>)
  可能大多数读者对上面这个类似公式一样的东西会不太理解。下面分析一下。首先,WebDriverWait是Python的一个类,从以上用法中可以看出,这个类的构造器带有两个参数:一个是driver,也就是一开始创建Firefox浏览器的driver对象;另一个是timeout,也就是等待的超时时间。然后,我们可以看到后面跟了一个until函数,其参数是condition,指的是需要等待的条件,这个条件的设置可以非常灵活,各式各样的条件都可以随意指定。不过在揭晓其用法之前,先看一下WebDriverWait类的源代码,以便更加深入地了解,如图2.14所示。
  图2.14  WebDriverWait类的源代码
  图2.14是WebDriverWait类的构造器。原来这个类的构造器的参数不止两个,它一共有4个参数,只是另外两个参数为可选参数。这两个参数中的第1个参数为poll_frequency,它表示等待过程中多久检查一次条件是否满足,可以从注释中看到默认是0.5s;第2个参数为ignored_exceptions,意思是等待过程中的每一个检查状态都会被忽略的异常,默认这个异常只包含NoSuchElementException异常,意思是“找不到元素”。测试人员在实际工作中看到最多的异常应该就是它了,此处包含这个异常是为了保证在等待某个对象出现的过程中,如果出现这个异常,就会自动忽略它。当然,在实际工作中初始化WebDriverWait时基本上只会用到两个默认参数,至于另外两个参数,读者可以具体情况来自行判断是否需要加入或者修改。
  在了解了WebDriverWait这个类的构造器后,接下来分析一下WebDriverWait类中的until函数,看一下它对应的Selenium源代码(如图2.15所示)。
  在图2.15的源码中可以看到,until函数包含3个参数,第2个参数表示一个方法,第3个参数是字符串message。该函数的功能是调用传入的方法,并返回一个布尔值。如果返回值为True,until函数直接返回停止等待;如果返回值为False,程序就一直执行while循环,直到其传入的方法返回True或者超时为止。这个超时的timeout时间即为之前创建WebDriverWait对象时传入的第2个参数,因此在代码最后一行可以看到,until函数一旦超时,就会跳出循环并抛出TimeoutException异常。了解了源代码如何实现之后,再来看一下具体如何使用私人订制等待。在使用WebDriverWait调用私人订制等待脚本同步时通常会有两种常用的方法。
  图2.15  until函数的源代码
  1.使用自带的Expected_Condition模块
  为了帮助读者理解,此处特地准备了一个相对简单且非常特殊的例子,建议读者实际操作,这样才能真正体会到其强大的作用。
  实现步骤如下。
  (1)打开浏览器,并跳转到Mercury Tours登录页面。
  (2)脚本会一直处于等待状态,直到用户在UserName文本框内输入mercury字符串。
  (3)一旦用户成功在UserName文本框中输入了mercury,脚本立即停止等待。
  (4)输入密码mercury并单击Login按钮。
  代码实现
   from selenium import webdriver
  from selenium.webdriver.common.by import By
  from selenium.webdriver.support.ui import WebDriverWait
  from selenium.webdriver.support import expected_conditions as EC
  driver = webdriver.Firefox()
  driver.get("Mercury Tours登录页面")
  try:
  element = WebDriverWait(driver, 10).until(
  EC.text_to_be_present_in_element_value((By.NAME, 'UserName'), 'mercury')
  )
  password_textbox = driver.find_element(By.NAME,'password')
  login_button = driver.find_element(By.NAME, 'login')
  password_textbox.send_keys("mercury")
  login_button.click()
  finally:
  driver.quit()
  直接运行以上脚本,在其运行过程中,读者会看到启动浏览器并跳转到Mercury Tours登录页面,接着就没有动静了,整个脚本就会保持一个等待的状态。这是因为我们在脚本中加入了一行私人订制等待代码,而这行代码的作用是,一直持续等待,直到在UserName文本框中输入mercury字样才会继续执行后续的脚本。当然,前提是设置了超时时间,等待超过 10s 就会抛出超时异常。读者可以尝试运行此脚本,接着手动在UserName文本框中输入mercury,从而完成整个脚本的运行。试着做一遍,看一下效果是否如上述那样,这一点可是智能全局等待完全无法做到的。
  为了说明得更加清楚一些,此处特地把私人订制等待的代码单独拿出来分析。代码如下。
   element = WebDriverWait(driver, 10).until(
  EC.text_to_be_present_in_element_value((By.NAME, 'UserName'), 'mercury')
  )
  此处的EC就是脚本已经导入的Expected_Condition模块,后面跟了很长一串的名字,其实这是该模块下的类名,从字面意思就能知道,此处用于判断一个元素的值中是否存在指定值。此类在实例化时,构造器包含了两个参数,第1个参数是定位器,也就是(By.NAME, 'UserName'),通过这个定位器可以找到对象;第2个参数是对象的值,一旦输入了这个值,等待就结束。那么怎么知道这些方法到底有几个参数呢?很简单,查看Selenium的Python文档,但是为了让读者了解得更深入,这里分析一下Selenium源代码的expected_condition.py文件。内容如图2.16所示。
  图2.16  expected_condition.py文件的内容
  这个py文件包含了许多不同的类。由于篇幅有限,我们就不一一全部列出来了,读者理解了其中一个,就能理解全部的类到底在做什么,并且会懂得如何使用它们。这里就从第一个类开始分析,这个类的构造器包含一个参数title,这样我们就知道在使用它的时候只需要一个参数(title)。接着你会发现一个很特别的方法—_ _call_ _,它是Python类中的一种特殊函数,那么这个_ _call_ _又是什么意思呢?其意思就是这个类的实例本身可直接当作函数调用。还记得吗?之前提到的until方法中的第1个参数恰巧传入的是一个函数,说到这里,读者应该明白为什么之前会把一个类实例作为until的参数了。一旦理解了这一点,你就会知道如何使用每一个类,并将这些类应用到私人订制等待中。
  title_is的作用是一直等待,直到title为title name为止。实现代码如下。
   element = WebDriverWait(driver, 10).until(
  EC.title_is("title name")
  )
  title_contains的作用是一直等待,直到title包含title name为止。实现代码如下。
   element = WebDriverWait(driver, 10).until(
  EC.title_contains("title name")
  )
  presence_of_element_located的作用是一直等待,直到元素存在为止。实现代码如下。
   element = WebDriverWait(driver, 10).until(
  EC.presence_of_element_located((By.NAME,"userName"))
  )
  在此,介绍这3个类就已经足够读者了解py文件如何使用了,限于篇幅,这里不再一一引出所有类,感兴趣的读者可以自行去尝试和理解。相信只要读者仔细琢磨并理解了其中原理,熟练使用整个expected_condition.py文件里的所有类只是时间问题。
  2.使用lambda函数表达式实现真正的自定义私人订制等待
  相信读者已经了解怎么实现上面的方法了,但是其实这里所做的都是在调用官方源代码提供的类,从严格意义上来说,并没有真正实现私人订制等待。其实要做到这一点也不难,还记得WebDriverWait类下的until方法中需要以一个函数作为参数吗?也就是说,直接传入一个lambda函数表达式就可以了。下面我们直接用lambda函数表达式来实现上面的方法中的实例。
  实例1:实现EC中判断文本是否在文本框中的同步方式。
  实现代码如下。
   from selenium import webdriver
  from selenium.webdriver.common.by import By
  from selenium.webdriver.support.ui import WebDriverWait
  driver = webdriver.Firefox()
  driver.get("Mercury Tours登录页面")
  try:
  element = WebDriverWait(driver, 10).until(
  lambda x: "mercury" in x.find_element(By.NAME, "userName").get_attribute("value")
  )
  password_textbox = driver.find_element(By.NAME,'password')
  login_button = driver.find_element(By.NAME, 'login')
  password_textbox.send_keys("mercury")
  login_button.click()
  finally:
  driver.quit()
  此lambda函数带有一个driver参数,函数体内一共做了3件事:第一,查找属性name为userName的对象;第二,获取此对象的value属性值;第三,判断此value值是否包含了mercury字符串。此函数最终返回的是一个布尔类型的值,直到这个布尔类型值为True或者超出超时时间。这只是一个最简单的例子,读者有兴趣的话可以自己尝试一些更加复杂的函数,实际工作当中经常会需要自己订制一些等待条件来保证正确的同步过程。下面给出实例2,在这个实例中使用EC是无法完成任务的。
  实例 2:一直等待直到UserName与password两个文本框同时满足value值都为mercury。
  实现代码如下。
   from selenium import webdriver
  from selenium.webdriver.common.by import By
  from selenium.webdriver.support.ui import WebDriverWait
  driver = webdriver.Firefox()
  driver.get("Mercury Tours登录页面")
  try:
  element = WebDriverWait(driver, 10).until(
  lambda x: "mercury" in x.find_element(By.NAME, "userName").get_attribute("value") \
  and "mercury" in x.find_element(By.NAME,"password").get_attribute("value")
  )
  login_button = driver.find_element(By.NAME, 'login')
  login_button.click()
  finally:
  driver.quit()
  要对两个元素的条件进行同步,利用自定义lambda表达式实现非常简单,只需要在lambda表达式的函数体内增加一个条件判断即可。通过这个例子,你应该能够体会,利用第二种方式可以灵活处理一些需要自定义的同步方式。
  在实际项目过程中,如果遇到同一类lambda表达式被反复使用的情况,读者完全可以尝试将自定义私人订制等待添加到官方源代码文件expected_condition.py中,从而自行扩展EC包中的一些自定义方法,记住正确实现__init__构造器及__call__方法即可,这样在最终脚本中,我们就可以在EC模块中调用自己扩展的自定义等待了。

查看《全栈软件测试自动化 Selenium和Appium (Python版)》全部连载章节
版权声明:51Testing软件测试网获得人民邮电出版社和作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责任。

《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号