专栏文章 [Web UI 自动化测试] 01 selenium 基础知识

几勺奶酪 · 2020年12月30日 · 1919 次阅读

定位方式

通过查看 selenium 源码中的 By 类,可以知道 selenium 中支持 8 种定位的方式

class By(object):
    """
    Set of supported locator strategies.
    """

    ID = "id"
    XPATH = "xpath"
    LINK_TEXT = "link text"
    PARTIAL_LINK_TEXT = "partial link text"
    NAME = "name"
    TAG_NAME = "tag name"
    CLASS_NAME = "class name"
    CSS_SELECTOR = "css selector"

然后通过查看 WebDriver 类下的 find_element 方法,可以明确 ID、TAG_NAME、CLASS_NAME、NAME 最终对应的底层策略都是 CSS_SELECTOR

def find_element(self, by=By.ID, value=None):
    """
    Find an element given a By strategy and locator. Prefer the find_element_by_* methods when
    possible.

    :Usage:
        element = driver.find_element(By.ID, 'foo')

    :rtype: WebElement
    """
    if self.w3c:
        if by == By.ID:
            by = By.CSS_SELECTOR
            value = '[id="%s"]' % value 
        elif by == By.TAG_NAME:
            by = By.CSS_SELECTOR
        elif by == By.CLASS_NAME:
            by = By.CSS_SELECTOR
            value = ".%s" % value
        elif by == By.NAME:
            by = By.CSS_SELECTOR
            value = '[name="%s"]' % value
    return self.execute(Command.FIND_ELEMENT, {
        'using': by,
        'value': value})['value']

css selector

定位速度比 xpath 快,但支持 web 和 webview 在原生的 app 上不支持

选择器 例子 描述
.class .intro 所有 class='intro'的元素,等价于 [id='intro']
#id #name 所有 id='name'的元素,等价于 [class='name']
* * 所有元素
element p 所有<p>元素
element,element div,p 所有<div>和<p>元素
element element div p <div>元素内部的所有<p>元素,即在子孙节点
element>element div>p 父元素为<div>元素的所有<p>元素,即父子节点
element+element div+p 跟着<div>元素的所有<p>元素,即兄弟节点
[attribute] [target] 所有带有 target 属性的元素
[attribute=value] [target=_blank] 所有 target 属性值为_blank 的元素
:nth-child(n) p:nth-child(2) 位于父元素的第二个子元素的

元素

:nth-last-child(n) p:nth-last-child(2) 位于父元素的倒数第二个子元素的

元素

element1~element2 p~ul 选择前面有<p>元素的每个<ul>元素

xpath

表达式 结果
nodename 选取此节点的左右子节点
/ 从根节点选取
// 子孙节点
. 当前节点
.. 当前节点的父节点
@ 选取属性

示例:

表达式 结果
/bookstore/book[1] 选取 document 下 bookstore 节点下的第一个 book 元素
/bookstore/book[last()] 选取 document 下 bookstore 节点下的最后一个 book 元素
/bookstore/book[position()<3] 选取 document 下 bookstore 节点下的前两个 book 元素
/bookstore/book[price>35.00] 选取 document 下 bookstore 节点下的 price 元素大于 35 的 book 元素
//title[@lang='eng'] 选取 doucumen 下所有子孙节点中属性 lang 为 eng 的 title 元素

隐式等待与显示等待

time.sleep()

与 java 语言一样,python 语言中也存在 sleep 函数,用于让当前线程休眠,不同的是 python 中的 sleep 函数的单位默认为秒

time.sleep(3) # 强制等待3秒

但是这个方法有个问题,即使一开始就找到元素了,仍然会等待 3 秒。

隐式等待

selenium 中提供了更高效的等待方式,隐式等待,它会设置一个等待时间,轮询查找(默认 0.5 秒)元素是否出现,如果没出现就抛出异常,单位默认为秒。

self.driver.implicitly_wait(3)

这是一个全局的等待时间,即一旦设置这个时间,每次进行元素查找的时候,都会在这个等待时间内进行轮询,直到找到元素。

但是这个方法也存在一定的问题,就是无法更加精确的对元素进行判断,列入有些元素可见但是不一定可点击。

显示等待

在代码中定义等待条件,当条件发生时才继续执行代码

WebDriverWait 配合 until() 和 until_not() 方法,根据判断条件进行等待,程序每隔一段时间(默认为 0.5 秒)进行条件判断,如果条件成立,则执行下一步,否则继续等待,直到超过设置的等待时间,抛出异常,单位默认为秒。

wait = WebDriverWait(driver=self.driver, timeout=10)
wait.until(expected_conditions.visibility_of_element_located(
    self.driver.find_elements_by_xpath('//*[@class="style__clients___iw5uL"]')))

上面代码中设置了一个超时时间 10 秒,然后会一直等待到该元素位置可见。

WebDriverWait

POLL_FREQUENCY = 0.5  # 轮询时间
IGNORED_EXCEPTIONS = (NoSuchElementException,) # 忽略哪些异常

class WebDriverWait(object):
    def __init__(self, driver, timeout, poll_frequency=POLL_FREQUENCY, ignored_exceptions=None):
    ...

从 WebDriverWait 的源码中可以看到,其轮询的默认时间为 0.5s,可以通过参数 poll_frequency 进行设置;其默认是会抛出所有异常的,也可以通过 ignored_exceptions 参数去指定忽略哪些异常

expected_conditions

WebDriverWait 会结合 expected_conditions 模块去对当前元素的状态进行检查,主要方法如下:

class url_contains(object): # url包含什么
class url_matches(object): # url匹配正则表达式
class url_to_be(object): # url是什么

class title_is(object): # 浏览器标题是什么
class title_contains(object): # 浏览器标题包含什么

class presence_of_element_located(object): # 元素在页面的DOM结构上是存在的,并不意味着可见
class visibility_of_element_located(object):# 元素在页面上是可见的,即元素的display属性不为none或者visibility不为hidden等隐藏元素,此外,该元素的长和宽的尺寸都大于0
class text_to_be_present_in_element(object): # 元素中出现具体的文字信息,即元素的text方法可以取到值
class text_to_be_present_in_element_value(object): # 元素的value属性中出现具体的文字信息,即元素的get_attribute("value")方法可以取到值
class invisibility_of_element_located(object): # 元素不可见,或不在DOM结构中

class element_to_be_clickable(object): # 元素可以被点击,意味着元素是可见的,并且是可用的,即元素的disabled属性为false,这样才能点击它
class element_to_be_selected(object): # 元素是被选中的,即单元框或者多选框元素的checked属性为true

class new_window_is_opened(object): # 新的标签页被打开,通过判断窗口句柄是否增多    
class alert_is_present(object): # alert弹窗出现,通过switch_to.alert方法

控件交互 Actions

ActionChains

执行 PC 端的鼠标点击,双击,右键,拖拽等事件

原理:调用 ActionChains 的方法时,不会立即执行,而是将所有的操作按顺序存放在一个队列里,当调用 perform() 方法时,队列中的事件会依次执行

def test_play(self):
    self.driver.get("https://music.163.com/#/playlist?id=3232747189")
    self.driver.switch_to.frame(self.driver.find_element_by_name("contentFrame"))
    self.driver.find_element_by_xpath('//*[@id="content-operation"]//a[@data-res-action="play"]').click()

    self.driver.switch_to.default_content()
    # 点击后,确保当前用例播放栏不被隐藏
    self.driver.find_element_by_xpath('//*[@data-action="lock"]').click()
    progress_btn = self.driver.find_element_by_xpath('//*[@id="g_player"]//*[@class="btn f-tdn f-alpha"]')
    progress_bar = self.driver.find_element_by_xpath('//*[@id="g_player"]//*[@class="m-pbar"]')
    # 动作链
    chains = ActionChains(self.driver)
    chains.click_and_hold(progress_btn)
    # 针对progress_btn而言进行移动,此时以progress_btn为原点,向左移动(x轴)到progress_bar的一半,纵坐标(y轴)不变
    chains.move_by_offset(xoffset=progress_bar.size['width'] / 2, yoffset=0)
    chains.release()

    chains.perform()

上面的代码用于拖动进度条,构建了一个动作链:

  • click_and_hold 在某个元素上按下鼠标左键

  • move_by_offset 移动到什么坐标

  • release 松开鼠标左键

最后通过 perform 方法依次执行了动作链中的每个动作,效果如下:

Video_20201229222330

常用方法

  • click_and_hold(on_element=None) 在某个元素上按下鼠标左键,如果没有元素,则在鼠标当前位置
  • move_by_offset(xoffset, yoffset) 从鼠标的当前位置为原点,移动到坐标(xoffset, yoffset)处
  • release(on_element=None) 在某个元素上抬起鼠标左键,如果没有元素,则在鼠标当前位置
  • move_to_element(to_element) 将鼠标移动到某个元素上
  • click(on_element=None) 如果元素为空,则在鼠标当前位置,点击鼠标左键;若元素不为空,则先执行 move_to_element(to_element) 方法,然后再点击鼠标左键
  • context_click(on_element=None) 如果元素为空,则在鼠标当前位置,点击鼠标右键;若元素不为空,则先执行 move_to_element(to_element) 方法,然后再点击鼠标右键
  • double_click(on_element=None) 如果元素为空,则在鼠标当前位置,双击鼠标左键;若元素不为空,则先执行 move_to_element(to_element) 方法,然后再双击鼠标左键
  • drag_and_drop(source, target) 将元素 source 拖拽到 target 元素处进行释放,等价于 click_and_hold(source),release(target)
  • drag_and_drop_by_offset(source, xoffset, yoffset) 将元素 source 拖拽到 (xoffset, yoffset) 坐标处进行释放,等价于 click_and_hold(source),move_by_offset(xoffset, yoffset), release()
  • key_down(value, element=None) 在当前元素或者当前焦点元素,按下键盘上某个键(只能是 Control, Alt and Shift),键盘按键在 Keys 类中进行了定义
  • key_up(value, element=None) 在当前元素或者当前焦点元素,送开键盘上某个键,键盘按键在 Keys 类中进行了定义
  • send_keys(*keys_to_send) 通过键盘输入,键盘按键在 Keys 类中进行了定义,等价于 key_down()、key_up()

TouchActions

模拟 PC 和移动端的点击,滑动,拖拽,多点触控等多种手势操作,原理于 ActionChains 类似,先生成动作链,然后通过 perform 方法去依次执行每个动作

  • tap(on_element) 点击某个元素
  • double_tap(on_element) 双击某个元素
  • tap_and_hold(xcoord, ycoord) 在某个给定的(xcoord, ycoord)坐标位置点击不释放
  • move(xcoord, ycoord) 移动到给定的(xcoord, ycoord)坐标位置
  • release(xcoord, ycoord) 在给定的(xcoord, ycoord)坐标位置释放
  • scroll(xoffset, yoffset) 滑动到给定的(xcoord, ycoord)坐标位置
  • scroll_from_element(on_element, xoffset, yoffset) 从某个元素开始滑动到给定的(xcoord, ycoord)坐标位置
  • long_press(on_element) 长按某个元素
  • flick(xspeed, yspeed) 从当前屏幕任意位置开始手势滑动 (负数:向上滑动,正数:向下滑动)
  • flick_element(on_element, xoffset, yoffset, speed) 从某个元素位置开始手势滑动 (负数:向上滑动,正数:向下滑动)

switch_to

多窗口处理

当页面上某个可点击元素的 target 属性为"_blank"时,点击后就会打开一个新的标签页,例如网易云音乐工具栏上的 “商城” 按钮,点击后就会打开一个新的标签页 “云音乐商城”,此时如果相对新的标签页进行操作,就需要进行窗口的跳转。

from selenium import webdriver


class TestToolBar:
    def setup_class(self):
        self.driver = webdriver.Chrome(executable_path="D:\\87\\chromedriver.exe")
        self.driver.maximize_window()
        self.driver.implicitly_wait(10)

    def teardown_class(self):
        self.driver.quit() # 关闭所有的标签页,关闭浏览器

    def setup_method(self):
        self.driver.get("https://music.163.com/")
        self.current_window = self.driver.current_window_handle # 获取当前窗口句柄

    def teardown_method(self):
        self.driver.close()  # 关闭当前标签页 
        self.driver.switch_to.window(self.current_window) # 切换到首页窗口

    def test_mart_link(self):
        self.driver.find_element_by_xpath('//*[@data-module="store"]').click()
        self.driver.switch_to.window(self.driver.window_handles[1])  # 获取所有的窗口句柄后,切换到第二个窗口句柄
        mart_link_title = self.driver.title
        assert "云音乐商城" in mart_link_title

    def test_musician_link(self):
        self.driver.find_element_by_xpath('//*[@data-module="musician"]').click()
        self.driver.switch_to.window(self.driver.window_handles[1])  # 获取所有的窗口句柄后,切换到第二个窗口句柄
        mart_link_title = self.driver.title
        assert "网易音乐人" in mart_link_title

执行接口如下:

test_tool_bar.py::TestToolBar::test_mart_link 
test_tool_bar.py::TestToolBar::test_musician_link 

============================= 2 passed in 18.42s ==============================

切换 iframe

有时候会存在这样的清空,刚打开页面的时候用$x(...)无法定位元素,当右键点击元素,选择检查后,再次使用$x(...)又可以定位成功了,然后在代码中通过 xpath 定位发现总是报错:NoSuchElementException。

这个时候就要看看这个元素的最外层的元素是否在<iframe>标签中,如网易云中的页面:

image-20201230001911178

如果我们要定位的元素位于<iframe>标签内,就需要先切换到 iframe,然后再进行元素定位:

def setup_method(self):
    self.driver.get("https://music.163.com/")
    # 无法定位元素的原因是因为嵌套在iframe中,需要先进入iframe再进行元素定位
    # 表现形式:通过$x('...')无法定位,当点击检查元素后,通过$x('...')又可以定位了
    self.driver.switch_to.frame(self.driver.find_element_by_name("contentFrame"))

def test_banner(self):
    self.driver.find_element_by_xpath('//*[@id="index-banner"]//img').click()
  • switch_to.frame(iframe_element) 切换到对应的 frame
  • switch_to.parent_frame() 切换到当前 iframe 的父级 iframe
  • switch_to.default_content() 切换到默认内容

切换 alert

有些情况下,点击按钮后会弹出 alert 确认框,此时需要对 alert 框进行处理,主要有如下方法:

# 相等于点击alert框的确定按钮
self.driver.switch_to.alert.accept()
# 相等于点击alert框的取消按钮
self.driver.switch_to.alert.dismiss()
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册