5.2 Selenium元素定位方法
在上一节中,我们学习了操作浏览器的基本方法和属性,接下来我们学习如何操作页面中的元素,毕竟我们手工测试所操作的对象就是页面中的各个元素。不过在开始学习操作各种页面元素之前,先介绍一下如何通过代码来找到这些页面元素。WebDriver提供了多种元素定位的方法,接下来一起来了解一下它们吧。
5.2.1 页面元素定位方法概览
我们通过PyCharm的自动补全代码功能来看一下WebDriver都提供了哪些元素定位方法,如图5-4所示。
图5-4 元素定位方法
我们可以看到,WebDriver一共提供了18种元素定位的方法。仔细观察可以发现,框中方法的element是单数形式,而框外则是elements复数形式。前者用来定位到单个元素,返回值的类型是WebElement;后者用来定位一组元素,返回值的类型是列表。我们可以通过下面的代码(test5_13.py)来确认。
from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get('https://www.baidu.com/') ele = driver.find_element_by_id('kw') print(type(ele)) eles = driver.find_elements_by_id('kw') print(type(eles)) driver.quit()
PyCharm控制台的输出结果如下。
<class 'selenium.webdriver.remote.webelement.WebElement'> <class 'list'>
5.2.2 使用id定位元素
简述:使用id定位百度的搜索框,输入“storm”。
使用开发者工具查看元素id属性,如图5-5所示。
图5-5 查看元素id属性
◆示例代码:test5_14.py
# 导入WebDriver包 from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get("http://www.baidu.com") # 定位百度搜索框 myinput = driver.find_element_by_name('wd') # 对其进行操作,输入"storm" myinput.send_keys("storm") # 等待2秒,可以发现搜索框中出现输入内容 sleep(2) # 退出浏览器进程 driver.quit()
◆运行结果
成功在百度的搜索框中输入“storm”。
◆注意事项
●请先忽略send_keys("storm"),后续我们会讲。
●正常来说,页面元素的id属性是唯一的,所以当页面元素存在id属性时,推荐大家使用id来定位元素。
5.2.3 使用name定位元素
简述:使用name定位百度搜索框,输入“storm”。
使用开发者工具查看元素name属性,如图5-6所示。
图5-6 查看元素name属性
◆示例代码:test5_15.py
# 导入WebDriver包 from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get("http://www.baidu.com") # 定位百度搜索框 myinput = driver.find_element_by_name('wd') # 对其进行操作,输入"storm" myinput.send_keys("storm") # 等待2秒,可以发现搜索框中出现输入内容 sleep(2) # 退出浏览器进程 driver.quit()
◆注意事项
●页面元素的name属性可能不是唯一值,在使用之前,需要通过搜索目标元素的name值来确定一下。
●在本小节中,百度搜索框的name属性值是“wd”,虽然搜索“wd”会出现多个结果,但是我们通过上下箭头查看,可以发现真正的name="wd"的元素只有一个,因此,我们可以使用上述的语句来定位该元素。
5.2.4 使用class name定位元素
简述:使用class name来定位页面中的元素。
◆示例HTML:myhtml5_1.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用class or tag name定位元素</title> </head> Please input your name: <input class="myclass"></input> </body> </html>
◆示例代码:test5_16.py
from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get('d:\\Love\\Chapter-4\\4-2\\test4_2-4.html') # 绝对路径加HTML文件 ele = driver.find_element_by_class_name('myclass') # 通过class name定位元素 ele.send_keys('storm') sleep(2) driver.quit()
◆注意事项
●如果要打开本地HTML文件的话,请使用绝对路径。
●Windows下文件目录是反斜线“\”,而我们知道“\”加部分字符是转义的意思,例如“\r”代表回车符。假如你的目录中某个文件夹的名字恰好是rock(以r开头),Selenium在寻找文件的时候就会报错。为了避免这种情况的发生,就需要使用双反斜线来取消转义。
●class name和name属性有些相似,一般来说,它们都不是唯一值,大家在使用该属性的时候需要注意该点。
5.2.5 使用tag name定位元素
简述:使用tag name来定位页面中的元素。
◆示例HTML:myhtml5_1.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>使用class or tag name定位元素</title> </head> Please input your name: <input class="myclass" name="myname" id="myid"></input> </body> </html>
◆示例代码:test5_17.py
from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get('d:\\Love\\Chapter-5\\test5_1.html') # 绝对路径加HTML文件 ele = driver.find_element_by_tag_name('input') # 通过tag name定位元素 ele.send_keys('storm') sleep(2) driver.quit()
想成功运行代码,必须要将配套文件放到Windows下的D盘中。其实要想让代码更健壮,我们可以这样来改动。
from selenium import webdriver from time import sleep import os driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_1.html' driver.get(html_file) # 绝对路径加HTML文件 # driver.get('d:\\Love\\Chapter-5\\test5_1.html') # 绝对路径加HTML文件 ele = driver.find_element_by_tag_name('input') # 通过tag name定位元素 ele.send_keys('storm') sleep(2) driver.quit()
◆注意事项
●我们前面学过,tag name是指元素的标签名,而页面中具有相同tag name的元素的概率就非常高了。因此,我们很少通过tag name来定位单个元素。
●本小节中,我们构造的HTML文件中只有一个<input>标签,因此可以使用tag name来定位元素。
5.2.6 使用链接的全部文字定位元素
简述:使用链接的全部文字定位元素。
使用开发者工具查看元素属性,如图5-7所示。
图5-7 查看文本
注意▶ 虽然在该页面中搜索到两个“新闻”文本,但其中一个并非页面元素,如图5-8所示,因此我们可以使用该文字来定位目标元素。
图5-8 非页面元素文本
◆示例代码:test5_18.py
from time import sleep driver = webdriver.Chrome() driver.get("http://www.baidu.com") # 单击百度首页右上角的“新闻”链接 ele = driver.find_element_by_link_text("新闻") ele.click() # 单击该链接 sleep(2) # 退出浏览器进程 driver.quit()
◆注意事项
最好使用开发者工具来复制<a>标签中的文字,不要仅通过浏览器页面上看到的文字去定位元素,因为浏览器会过滤掉一些空格字符。
5.2.7 使用部分链接文字定位元素
简述:使用部分链接文字定位百度首页右上角的“新闻”链接。
使用开发者工具查看元素属性,如图5-9所示。
图5-9 查看链接元素
◆示例代码:test5_19.py
from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get(“http://www.baidu.com”) # 单击百度首页右上角的“新闻”链接 ele = driver.find_element_by_partial_link_text(“闻”) ele.click() # 单击该链接 sleep(2) # 退出浏览器进程 driver.quit()
◆注意事项
链接的全部文字和部分文字的定位方法是针对“链接类”元素的有效定位方法。
5.2.8 使用XPath定位元素
简述:使用XPath定位百度首页搜索框元素,输入“storm”。
使用开发者工具查看并复制元素XPath,如图5-10所示,复制的XPath为“//*[@id="kw"]”。
图5-10 复制元素XPath
◆示例代码:test5_20.py
from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get("http://www.baidu.com") ele = driver.find_element_by_xpath('//*[@id="kw"]') # 使用XPath定位搜索框 ele.send_keys('storm') sleep(2) # 退出浏览器进程 driver.quit()
◆注意事项
这里我们只是简单演示了一下如何借助开发者工具来复制XPath。某些时候,我们需要手动编写更“合理”的XPath。有关XPath的知识,会在5.2.12小节进行详细讲解。
5.2.9 使用CSS定位元素
简述:使用CSS定位百度首页搜索框元素,并输入文字“storm”。
使用开发者工具查看并复制元素的CSS selector,如图5-11所示,复制后的CSS selector为“#kw”。
图5-11 复制CSS selector
◆示例代码:test5_21.py
from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get("http://www.baidu.com") ele = driver.find_element_by_css_selector('#kw') # 使用CSS来定位元素 ele.send_keys('storm') sleep(2) driver.quit()
◆注意事项
这里简单演示了一下如何使用开发者工具来复制CSS selector,有关手动编写CSS selector的知识,会在5.2.12小节进行详细详解。
5.2.10 使用find_element('locator', 'value')定位元素
我们可以通过find_element关键字来定位元素,把用到的locator和value以参数的方式传递进去。此方法的好处我们暂时不谈,先来学习一下用法吧。
传递locator和value的方式有两种:一种是直接传递;另一种是先引入By的包,然后借助By来传递。我们分别来看一下。
(1)直接传递
简述:使用find_element('locator','value')定位元素,locator和value直接传递。
◆示例代码:test5_22.py
from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get('D:\\Love\\Chapter-4\\4-2\\test4_2-4.html') # 直接传递locate type的方式只支持4种定位元素的方法,分别是id、name、XPath、CSS # driver.find_element('id', 'myid').send_keys('id') # 支持id # driver.find_element('name', 'myname').send_keys('name') # 支持name # driver.find_element('xpath','//*[@id=“myid”]').send_keys('xpath') # 支持XPath driver.find_element('css','#myid').send_keys('css') # 支持CSS # driver.find_element('tagname', 'input').send_keys('input') # 不支持 sleep(3) driver.quit()
注意▶ 直接传递locate type的方式只支持4种定位元素的方法,分别是id、name、XPath、CSS。
(2)借助By来传递
简述:通过引入common.by的包,可以完美支持前面讲过的8种定位元素的方法。
◆示例代码:test5_23.py
from selenium import webdriver from selenium.webdriver.common.by import By from time import sleep driver = webdriver.Chrome() driver.get('D:\\Love\\Chapter-4\\4-2\\test4_2-4.html') # 通过引入common.by的包,可以完美支持前面讲过的8种定位元素的方法 # driver.find_element(By.ID,'myid').send_keys('id') # 支持id # driver.find_element(By.NAME, 'myname').send_keys('name') # 支持name # driver.find_element(By.XPATH,'//*[@id=“myid”]').send_keys('xpath') # 支持XPath driver.find_element(By.TAG_NAME,'input').send_keys('tag name') # 支持tag name sleep(3) driver.quit()
◆注意事项
●首先需要引入By的包。
●传递locator的时候需要借助By提供的方法(此时定位器都用大写字母表示,比如“NAME”)。
5.2.11 定位组元素
在5.2.1小节中,我们了解到使用find_elements_xxx来定位一组元素时,该方法会将所有定位到的元素放到一个列表中,然后我们可以通过列表的下标定位到具体的元素。本小节我们就来看一下,这种方式到底是“画蛇添足”,还是“曲线救国”,又或者是某些特定场景下的“撒手锏”。
示例一:使用tag name定位百度搜索框元素,并输入文字。
在5.2.5小节中提到过,一个页面中元素的标签名非常容易重复,当时为了演示tag name定位元素的方法,我们自建了一个HTML文件,这个文件中只有一个元素的标签是<input>,而现在的任务却是通过tag name来定位百度搜索框元素。
来看一下百度首页有多少个<input>标签吧,如图5-12所示。
图5-12 百度首页具有的<input>标签
我们来数一下,搜索框是第8个<input>标签,那么我们是否可以用list[7]来定位它呢?
◆示例代码:test5_24.py
from selenium import webdriver from time import sleep driver = webdriver.Chrome() driver.get('https://www.baidu.com/') eles = driver.find_elements_by_tag_name('input') print(len(eles)) # 我们输出一下该页面有多少个<input>标签 eles[7].send_keys('storm') # 通过下标从列表中取单个元素进行操作 sleep(2) driver.quit()
◆运行结果
共计定位到16个<input>标签,通过下标7取到了第8个<input>标签,即百度搜索框,然后成功输入了“storm”。
示例二:模糊有时候更美丽。
这里有一个缺陷管理系统,要求验证全选功能是否正常,即单击全选复选框后,判断本页面所有的缺陷是否被选中,如图5-13所示。
图5-13 查看复选框元素
操作思路如下。
●进入该管理系统页面,定位全选复选框(该元素有id,且唯一),然后单击该复选框。
●接下来我们要定位每一个复选框,然后分别判断它们是不是处于选中状态。这里有两个问题:一是有多少个复选框不确定,二是依次定位每一个复选框的工作量较大。使用开发者工具查看每个有缺陷的复选框,可以发现它们都有一个name属性,且值都是“ids[]”。那我们是否可以定位组元素,然后再循环操作这个列表中的元素呢?
示例代码[test5_25.py(部分代码,无法运行)]如下。
lsts = driver.find_elements_by_name("ids[]") for lst in lsts: if lst.is_selected(): # 假如元素被选中 print('pass') # 输出pass else: # 否则 print('fail') # 输出fail
思考:假如上面的示例中,复选框元素没有name属性,我们是否可以通过tag name去进行定位呢?
组元素定位的方式暂时讲解到这里,在后续的章节中,我们还会遇到类似的问题,敬请期待。
小结:合理使用“单元素定位”和“组元素定位”的各种方法能帮我们解决掉实际项目中大多数元素定位的问题。
既然元素的id是唯一的,为什么还有find_elements_by_id这个方法呢?
元素定位方法如表5-3所示。
表5-3 元素定位方法
5.2.12 XPath和CSS selector精讲
XPath和CSS selector的基本概念在第4章中介绍过,这里不再赘述。本小节,我们通过一些实例来看看XPath和CSS selector在元素定位中的应用。
某些情况下,我们需要手动编写XPath或CSS selector。当一个元素无法直接定位,而是需要通过附近节点的元素来进行相对定位的时候,就需要我们掌握本小节的内容。
表5-4所示为XPath和CSS selector定位元素的方法。
表5-4 XPath和CSS selector定位元素的方法
续表
接下来我们用几个示例讲解一下定位方法。
(1)串联查找
◆目的
先定位一个元素,然后在其基础上定位一个元素。
◆示例HTML:myhtml5_2.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="A"> <div id="B"> <div>parent to child</div> </div> </div> </body> </html>
◆示例代码:test5_26.py
from selenium import webdriver from time import sleep import os ''' 串联查找 ''' driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_2.html' driver.get(html_file) mytext = driver.find_element_by_id('B').find_element_by_tag_name('div').text print(mytext) driver.quit()
◆运行结果
parent to child
(2)通过父元素定位子元素
◆示例HTML:myhtml5_2.html
◆示例代码:test5_27.py
from selenium import webdriver import os ''' 通过父元素定位子元素 ''' driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_2.html' driver.get(html_file) mytext = driver.find_element_by_xpath("//div[@id='B']/div").text print(mytext) driver.quit()
◆运行结果
parent to child
(3)借用xpath轴,通过父元素定位子元素
◆示例HTML:myhtml5_2.html
◆示例代码:test5_28.py
from selenium import webdriver import os ''' 借用xpath轴,通过父元素定位子元素 ''' driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_2.html' driver.get(html_file) mytext = driver.find_element_by_xpath("//div[@id='B']/child::div").text print(mytext) driver.quit()
◆运行结果
parent to child
(4)由子元素定位父元素
◆示例HTML:myhtml5_3.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8v> <title>Title</title> </head> <body> <div id="A"> <!--子元素定位父元素--> <div> <div>child to parent <div> <div id="C"></div> </div> </div> </div> </div> </body> </html>
◆示例代码:test5_29.py
from selenium import webdriver import os # 由子元素定位父元素 driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_3.html' driver.get(html_file) # xpath:'.'代表当前元素; '..'代表父元素 mytext = driver.find_element_by_xpath("//div[@id='C']/../..").text print(mytext) # xpath轴parent mytext1 = driver.find_element_by_xpath("//div[@id='C']/parent::div/parent::div").text print(mytext1) driver.quit()
◆运行结果
child to parent child to parent
(5)通过弟弟元素定位哥哥元素
◆示例HTML:myhtml5_4.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <!--下面两个元素用于兄弟元素定位--> <div>brother 1</div> <div id="D"></div> <div>brother 2</div> </body> </html>
◆示例代码:test5_30.py
from selenium import webdriver import os """ 通过弟弟元素定位哥哥元素 """ driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_4.html' driver.get(html_file) # xpath通过父元素获取哥哥元素 mytext = driver.find_element_by_xpath("//div[@id='D']/../div[1]").text print(mytext) # xpath轴preceding-sibling mytext1 = driver.find_element_by_xpath("//div[@id='D']/preceding-sibling::div[1]").text print(mytext1) driver.quit()
◆运行结果
brother 1 brother 1
(6)通过哥哥元素定位弟弟元素
◆示例HTML:myhtml5_4.html
◆示例代码:test5_31.py
from selenium import webdriver import os """ 通过哥哥元素定位弟弟元素 """ driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_4.html' driver.get(html_file) # xpath通过父元素获取弟弟元素 mytext = driver.find_element_by_xpath("//div[@id='D']/../div[3]").text print(mytext) # xpath轴following-sibling mytext1 = driver.find_element_by_xpath("//div[@id='D']/following-sibling::div[1]").text print(mytext1) driver.quit()
◆运行结果
brother 2 brother 2
(7)通过CSS selector来定位页面元素
◆示例HTML:myhtml5_5.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>test</title> </head> <body> please input your name:<input name="name" class="classname" > <br> <a href="http://map.baidu.com">I'm Storm</a> </body> </html>
◆示例代码:test5_32.py
from selenium import webdriver from time import sleep import os """ 通过CSS selector定位页面元素 """ driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_5.html' driver.get(html_file) driver.find_element_by_css_selector("body > input").send_keys('css') sleep(3) driver.quit()
◆运行结果
成功定位到搜索框,并且输入“css”。
(8)通过CSS selector定位页面元素
◆示例HTML:myhtml5_6.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="parent">parent <div id="brother1">brother 1</div> <div id="brother2">brother 2</div> </div> </body> </html>
◆示例代码:test5_33.py
from selenium import webdriver from time import sleep import os """ 通过CSS selector定位页面元素 """ driver = webdriver.Chrome() html_file = os.getcwd() + os.sep + 'myhtml5_6.html' driver.get(html_file) #通过父元素parent定位子元素brother1:nth-child() mytext = driver.find_element_by_css_selector("div#parent div:nth-child(2)").text print(mytext) driver.quit()
◆运行结果
brother 2
本小节重点给大家演示了串联查找、通过父元素定位子元素、通过子元素定位父元素、通过哥哥元素定位弟弟元素、通过弟弟元素定位哥哥元素的方法,希望大家灵活掌握。
5.2.13 Selenium 4的相对定位器
Selenium 4新增了相对定位器,能帮助用户查找元素附近的其他元素。可用的相对定位器有above、below、toLeftOf、toRightOf、near。在Selenium 4中,find_element方法能够接受一个新方法withTagName,它将返回一个RelativeLocator(关联定位器)。
下面来看一下新方法如何使用。
Selenium 4使用JavaScript的getBoundingClientRect方法来查找关联元素。这个方法会返回元素的属性,例如right、left、bottom、top。
来看下面这个示例。假如我们有一个页面,如图5-14所示,页面上有4个元素。
图5-14 示例页面
(1)above方法
该方法用来返回指定元素上方的元素。例如,我们可以先定位到Password输入框,然后通过above方法返回Password元素上方的tag name="input"的元素。示例代码如下。
#from selenium.webdriver.support.relative_locator import with_tag_name passwordField = driver.find_element(By.ID, "password") emailAddressField = driver.find_element(with_tag_name("input").above(passwordField))
(2)below方法
该方法用来返回指定元素下方的元素。例如,我们可以先定位到Email Address文本框,然后通过below方法返回Email Address元素下方的tag name="input"的元素。示例代码如下。
#from selenium.webdriver.support.relative_locator import with_tag_name emailAddressField = driver.find_element(By.ID, "email") passwordField = driver.find_element(with_tag_name("input").below(emailAddressField))
(3)toLeftOf方法
该方法用来返回指定元素左侧的元素。例如,我们可以先定位到Submit按钮,然后通过toLeftOf方法来返回Submit元素左侧的tag name="button"的元素。示例代码如下。
#from selenium.webdriver.support.relative_locator import with_tag_name submitButton = driver.find_element(By.ID, "submit") cancelButton = driver.find_element(with_tag_name("button").toLeftOf(submitButton))
(4)toRightOf方法
该方法用来返回指定元素右侧的元素。例如,我们可以先定位到Cancel按钮,然后通过toRightOf方法返回Cancel元素右侧的tag name="button"的元素。示例代码如下。
//import static org.openqa.selenium.support.locators.RelativeLocator.withTagName; WebElement cancelButton= driver.findElement(By.id("cancel")); WebElement submitButton= driver.findElement(withTagName("button").toRightOf (cancelButton));
(5)near方法
该方法用来返回指定元素附近(最远50像素的距离)的元素。例如,我们可以先定位到Email Address元素,而该元素与下方文本框的距离小于50像素。因此,我们可以使用near方法来定位元素。示例代码如下。
//import static org.openqa.selenium.support.locators.RelativeLocator.withTagName; WebElement emailAddressLabel= driver.findElement(By.id("lbl-email")); WebElement emailAddressField = driver.findElement(withTagName("input").near (emailAddressLabel));
5.2.14 元素定位“没有银弹”
有的人说“让开发人员把所有元素都加上id”;有的人说“建议全部使用XPath来定位”;有的人说“建议使用CSS,CSS的效率更高”。我却想说,元素定位根本就“没有银弹”。“什么?你花了很大的篇幅来介绍元素定位的方法,现在却要告诉我,无法找到一个完美的、牢不可破的元素定位器?”
说“无法找到一个完美的、牢不可破的元素定位器”,是因为不同的定位器适用于不同的场景,这也是我们用巨大的篇幅来介绍这些方法的原因。
在实际项目中,无论是项目页面的重新设计,还是功能的迭代,都有可能导致之前开发的脚本运行失败,这是我们必然会面临的一个问题。实际上,相当多的项目组在推行自动化测试时失败的一个重要原因就是,脚本的可扩展性太低,同时维护成本太高。但是好的定位方法和坏的定位方法是有迹可循的,这意味着当你熟练掌握了前面所学的内容,再依据本小节的一些“条例”,就可以大概率地设计出一个“完美”的定位器。
(1)id是首选
id是最安全的定位选择,应该始终是你的首选。按照万维网联盟(World Wide Web Consortium,W3C)的标准,一个页面中的元素的id值是独一无二的,这意味着你永远不会遇到通过id匹配到多个元素的问题。另外,i d也是独立于树外的元素定位方法,因此即便开发人员调整了元素的位置或者更改了元素的类型,WebDriver仍然可以通过id找到它。
id经常用于Web页面的JavaScript中(事实上,大多数开发人员并不是同情测试人员定位元素困难才给元素加上id的,而是他们自己需要用到才加上的),开发人员会避免更改元素的id,因为他们不想更改自己的JavaScript代码。不管怎样,这对测试人员来说太棒了。
所以某些文章的作者告诉你“建议开发人员将所有元素加上id的属性”“和开发人员约定,所有核心页面元素都添加id属性”,这些是不错的想法,但实际上是“异想天开”。在大多数的项目中,你只能变通一下,例如在元素没有id的时候,使用XPath或CSS来定位。
(2)XPath和CSS
XPath和CSS定位器在概念上非常相似,所以我们把它们放在一起讨论。这些类型的定位器组合了标签名、后代元素、CSS的class name或元素属性,使用起来非常灵活,但灵活带来的问题就是要么匹配元素过于严格,要么匹配元素过于宽松。“严格”的坏处是小的HTML页面的变化都会使其失效,而过于“宽松”又可能会匹配多个HTML元素。所以当编写CSS或XPath定位器时,我们需要在“严格”和“宽松”之间找到平衡点,使其既能够尽可能适应HTML的变化,又能在应用程序出现错误时,及时准确地反映出来。
◆借助锚点元素定位
当你打算使用CSS或XPath来定位元素的时候,建议你先找到一个不太可能改变的元素作为锚点,然后再来定位目标元素。这个锚点可能有id或者处于稳定的位置(不易改变,例如某个网页的头图,即便没有id,在大多数情况下,头图的位置也不会改变)。
锚点元素虽然不是目标元素,却是一个非常可靠的“中转站”。锚点元素可以在目标元素的前面,也可以在后面,但是大多数情况下建议选择目标元素上面的元素作为锚点。
示例HTML:myhtml5_7.html。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>锚点定位</title> </head> <body> <div id="main-section"> <p>Introduction</p> <ul> <li> Option 1</li> </ul> </div> </body> </html>
在本例中,我们想要定位<li>元素,它没有id、class name等属性,尤其是某些情况下页面中还有多个<li>元素,因此很难定位。但我们发现div有个id="main-section",这时候,我们就可以将其选为寻找<li>元素的锚点元素,这样就缩小了定位器正在搜索的HTML的范围(从锚点往下找,不用找整个HTML)。
◆合理使用“索引”定位器,如nth-child()和[x]
nth-child()、first-child、[1]等,这些索引类型的定位器只能应用于列表对象。当你使用它们时,你首先要确定自己的测试目标,如果你的目标只是验证搜索结果的某个特定位置(例如不管第一个位置是谁,只想要第一个),那么你用索引肯定是没问题的;但如果你的目标是某个特定的元素(例如有A、B、C 3个元素,不管它们怎么摆放,我都只想定位A元素),为了避免元素的顺序发生改变后你的定位器出现失效的问题,应该尽量避免使用索引来定位。示例HTML:myhtml5_8.html。
<menu> <button>Option 1</button> <button>Option 2</button> <button>Option 3</button> </menu>
不管列表项目如何排序,如果你只想要与第一个菜单项进行交互,那么//menu/button[1]是一个合适的定位器。大多数情况下,我们只是想选择列表中的某一项,如这里的Option 1。如果button的顺序改变成下面这样(myhtml5_9.html),你还通过“//menu/button[1]”进行定位,就会定位到Option 3,导致测试失败。
<menu> <button>Option 3</button> <button>Option 2</button> <button>Option 1</button> </menu>
这种变化你的测试过程能接受吗?还是说你需要重新编写定位器?这种情况下,非索引定位器//menu/*[text()='Option 1']可能更合适。这时候<menu>是理想的锚点元素。
◆CSS的class name具有一定的意义
前端设计人员经常会给CSS的class name赋予一定的意义,我们应该充分利用这一点。来看下面的示例HTML:myhtml5_10.html。
<footer class="form-footer buttons"> <div class="column-1"> <a class="alt button cancel" href="#">Cancel</a> </div> <div class="column-2"> <a class="alt button ok" href="#">Accept</a> </div> </footer>
在上面这个示例中,忽略class="column-1"和class="column-2",这两个class name用得比较好,原因如下:div指的是布局,因此当开发人员决定调整设计(调整布局)的时候,它们可能会受到影响。如果我们能直接对目标按钮(Accept和Cancel按钮)进行操作会更加可靠。虽然“alt button ok”在页面上可能不止一个,看起来像个宽松的定位器,但是你可以将<footer>作为锚点元素,在这个例子中,“footer .ok”是一个好的定位器。
◆提前规避易变的事情
细心对比一下下面的HTML(myhtml5_11.html)和刚才的HTML(myhtml5_10.html)。
<footer class="form-footer buttons"> <div class="column-1"> <a class="alt button cancel" href="#">Cancel</a> </div> <div class="column-ok"> <button class="alt ok">ok</button> </div> </footer>
我们发现tag name从<a>变成了<button>,class的值从“"alt button ok"”变成了“"alt ok"”,标签的文本也从“Accept”变成了“ok”。结果导致class name、text content、tag name都匹配不到了。
如果我们换成宽松的定位器呢?这样就能容忍HTML中的一些变化。接下来让我们在宽松的定位器上犯错。
直接派生类(通过父节点找子节点),示例如下。
CSS example: div > div > ul > li > span Xpath example: //div/div/ul/li/span
直接派生类是指HTML元素的父子关系。在上述CSS example示例中,<li>是<url>的子元素。
回到上面的示例中,使用长链的话可能会帮助你找到一个没有class name或者id的元素,但从长远来看,它肯定是不可靠的。在没有id或class name的情况下,大段的内容是经常动态变化的,而且可能经常移动或在HTML中变换位置。长链对所有的这些变化都将无能为力,如果你不得不使用直接派生类的定位器,那么尽量尝试在每个定位器中最多使用一个层级。
(3)根据实际目标调整
来看下面这段HTML代码(myhtml5_12.html)。
<section id="snippet"> <div>Blurb</div> </section>
建议只使用你需要的定位器,少就是多。如果你只是想捕获<section>这个标签的文本(即Blurb),那么使用“# snippet div”这样的定位器是没有必要的。因为对于WebDriver来说,定位器“# snippet”和“#snippet > div”返回的文本内容相同,但是如果<div>元素被更改为<p>或<span>,则后者将会失效。
(4)目标元素的属性
使用目标元素的属性来定位和使用CSS的class name定位非常类似,属性可以是唯一的,但有的时候也会在许多项目中重复使用,必须根据情况来确定到底什么时候用它。
一般来说,最好避免使用属性,只关注id、tag name和CSS类。但在HTML 5中,数据属性是稳定的,因为它们与Web应用程序的功能紧密结合在一起。
(5)tag name、link text、name的定位策略
这些定位策略只是通过属性或文本字符串查找元素的快捷方式[XPath:text()]。大家需要根据实际情况,权衡使用这些定位策略。
综上所述,当你在编写一个定位器时,有如下建议需要考虑。
●优先使用id定位。
●使用有id的附近元素作为锚定元素来定位。
●搞明白定位器的用途——它仅仅是为了导航还是做断言?如果元素移动或测试失败,定位器是否能够处理?定位器的用途将决定你需要在定位器中使用的技术有多“严格”或多“宽松”。
提醒:请务必掌握单个元素和元素组的定位方法;没有万能的定位方法,只有合适的定位方法;XPath和CSS非常重要,可以根据自己的习惯熟练掌握其一。