Python实现Web UI自动化测试实战:Selenium 3/4+unittest/Pytest+GitLab+Jenkins
上QQ阅读APP看书,第一时间看更新

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非常重要,可以根据自己的习惯熟练掌握其一。