[TOC]

本文是学习了《Selenium自动化测试完全指南》和其他网络上的教程写出的学习笔记

测试的基本要素

测试最基本的两个要素是测试输入和预期输出结果。

案例:异步社区登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#coding: utf-8
import time

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait

wd = webdriver.Chrome()
wd.get("https://www.epubit.com/")

# 测试输入:登录操作
WebDriverWait(wd,5).until(lambda p:p.find_element(By.XPATH,"//div[contains(@class,'login')]/i[1]").is_displayed())

wd.find_element(By.XPATH,"//i[text()='登录']").click()
wd.find_element(By.ID,"username").send_keys("18732566535")
wd.find_element(By.ID,"password").send_keys("15930571421swSW")
wd.find_element(By.ID,"passwordLoginBtn").click()

# 预期输出结果:验证
isJumpToHomePage = wd.current_url == 'https://www.epubit.com/' #是否跳转回主界面
isShowUserImg = wd.find_element(By.XPATH,"//div[contains(@class,'userLogo')]/img").is_displayed()
# 主界面是否显示登录用户的头像图片
isShowExit = wd.find_element(By.XPATH,"//div[contains(@class,'logout')]/div[2]").is_displayed()
# 是否有退出按钮
time.sleep(3)
print(isShowExit,isShowUserImg,isJumpToHomePage)
wd.quit()

我们的预期结果是查看主界面是否有下面这两个元素

image-20230822123300619

以上代码就算完成了一个基本的登录测试,但并没有使用任何测试框架,为了完善测试的机制,实现更丰富的测试功能,可以引入比较成熟的测试框架。这里将使用pytest框架

基于Pytest编写Selenium测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# test_epubit_common.py文件
# coding: utf-8
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait

class TestEpubitCommon:
def test_epubit_login(self):
wd = webdriver.Chrome()
wd.implicitly_wait(5)
wd.get('https://www.epubit.com/')
WebDriverWait(wd, 5).until(
lambda p: p.find_element(By.XPATH, "//div[contains(@class,'login')]/i[1]").is_displayed())

wd.find_element(By.XPATH, "//i[text()='登录']").click()
wd.find_element(By.ID, "username").send_keys("18732566535")
wd.find_element(By.ID, "password").send_keys("15930571421swSW")
wd.find_element(By.ID, "passwordLoginBtn").click()

# 验证
isJumpToHomePage = wd.current_url == 'https://www.epubit.com/'
isShowUserImg = wd.find_element(By.XPATH, "//div[contains(@class,'userLogo')]/img").is_displayed()
isShowExit = wd.find_element(By.XPATH, "//div[contains(@class,'logout')]/div[2]").is_displayed()
assert isShowExit and isShowUserImg and isJumpToHomePage
wd.quit()

运行结果

image-20230822131402490

如果我们需要进行其他测试功能的话,需要在这基础上再添加操作就可以了,但这样会使代码变得冗长,难以维护,并且也没有健壮性,一点小小的改动就要重写,非常不规范。所以有了自动化测试驱动

自动化测试驱动

软件自动化测试驱动模式的发展大致经历了4个阶段:线性测试;模块化与库;数据驱动;关键字驱动。

线性测试阶段

写线性脚本

通过录制回放操作,产生线性脚本。

在这种模式下,数据和脚本混在一起,几乎一个测试用例对应一个脚本,维护成本很高。即使界面的变化十分简单也需要重新录制,脚本的重复使用率低。

模块化与库

就是模块化编程思想

将测试分成过程和函数两部分。这种框架要求创建代表测试下的应用程序模块、零件和函数的库文件(SQABasic libraries、API、DLL等),然后让测试用例脚本直接调用这些库文件。通过这样的方式,就产生了可复用的函数或库文件,各个功能可独立维护,并能重复使用。

数据驱动

数据与代码分离

模块化与库很好地解决了用例重用性的问题。但是不难发现,在用例中,测试的操作和测试的数据是放在一起的,一旦需要对大量不同的数据进行测试,就得重新编写大量的用例

pytest提供了装饰器@pytest.mark.parametrize()

1
2
3
4
@pytest.mark.parametrize('参数a,参数b,参数c,...',[(1组参数a的值,参数b的值,参数c的
值,...),(2组参数a的值,参数b的值,参数c的值,...),...])
def test_测试函数(self, 参数a, 参数b, 参数c,...):
......

关键字驱动

用关键字理顺代码逻辑

关键字驱动框架是在数据驱动框架的基础上改进的一种框架模型。它将测试逻辑按照关键字进行分解,形成数据文件与关键字对应封装的业务逻辑。关键字主要包括3类,分别是被测试对象(object)、操作(action)和值(value),实现界面元素名与测试内部对象名分离,测试描述与具体实现细节分离。

举例:登录操作

Page Object Action Value
用户登录 用户名 输入input shenyunmomie
用户登录 密码 输入input 12345678
用户登录 登录 点击click
首页 验证Verify

Page Object Action Value就是关键字。

当测试需要在多种不同的环境下运行时,若测试中有一些数据或信息会因为环境的不同而失效,就需要考虑创建配置文件。

配置文件

  • 公共信息

有一些信息属于公共信息,这些信息可能在多个测试中使用,我们可以新建一个名为commonInfo.py的配置文件,将各类公共信息存放到该文件中,代码如下。

1
2
3
class CommonInfo:
biz_home_page_url = "https://www.epubit.com/"
tech_implicitly_wait_seconds = 5
  • 用例数据
1
2
3
4
5
from commonInfo import CommonInfo

class TestCaseData:
test_epubit_login_data = [(CommonInfo.biz_home_page_url, "yibushequUser1", "yibushequPwd1")]
test_book_search_data = [(CommonInfo.biz_home_page_url, "selenium"), (CommonInfo.biz_home_page_url, "python")]

测试的前置操作与后置操作

在Pytest当中,每一个测试开始执行前或结束执行后,都可以设定一些前置或后置性的操作.对于Selenium测试,可以很好地使用Pytest的这项特性,将一些公共性的操作或者设置,放置到这些前置或后置操作中。

setup与teardown功能详解

Pytest支持各个级别的前置或后置操作,只要函数命名和位置遵循以下规则,Pytest会自动将其识别为前置函数或后置函数。

1
2
3
4
5
6
7
8
setup_class()/teardown_class()
#在开始执行测试类中的首个测试函数前执行或在执行完测试类中的全部测试函数后执行
setup_method()/teardown_method()
#在执行测试类中的每个测试函数前/后都会执行
setup_module()/teardown_module()
#在整个.py模块开始运行前/结束运行后执行
setup_function()/teardown_function()
#执行测试类之外的每个测试函数前/后都会执行。

前后置操作实际运用案例

对于Web应用程序来说,部分页面涉及用户权限。在测试这些页面之前,都需要进行登录等前置操作,同时,在测试结束后,还需要执行一些清理性的后置操作。

案例:获取登录的个人信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# coding:utf-8

import pytest
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By


class TestEpubit:

def setup_class(self,homeurl='https://www.epubit.com/',username='*******6535',password='***'):
'''
测试前置操作:登录
:param homeurl:
:param username:
:param password:
:return:
'''
self.wd = webdriver.Chrome()
self.wd.implicitly_wait(5)
self.wd.get(homeurl)

WebDriverWait(self.wd,5).until(lambda p: p.find_element(By.XPATH,"//div[contains(@class,'login')]/i[1]").is_displayed())
self.wd.find_element(By.XPATH, "//i[text()='登录']").click()
self.wd.find_element(By.ID, "username").send_keys(username)
self.wd.find_element(By.ID, "password").send_keys(password)
self.wd.find_element(By.ID, "passwordLoginBtn").click()

def teardown_class(self):
'''
测试后置操作:退出
:return:
'''
self.wd.quit()

def test_userinfo(self):
'''
获取个人信息
:return:
'''

self.wd.find_element(By.XPATH, "//div[contains(@class,'userLogo')]").click() #点击头像
self.wd.find_element(By.XPATH,'//a[contains(@href,"/user/myInfo")]').click() #点击个人信息

# 等待页面加载
WebDriverWait(self.wd,5).until(lambda p:p.find_element(By.XPATH,'//span[text()="用户名:"]/following::span[@class="content"]').is_displayed())
# 获取个人信息的三个数据
name = self.wd.find_element(By.XPATH,'//span[text()="用户名:"]/following::span[@class="content"]').text
phoneNum = self.wd.find_element(By.XPATH,'//span[text()="手机号:"]/following::span[@class="content"]').text
mailNum = self.wd.find_element(By.XPATH,'//span[text()="电子邮箱:"]/following::span[@class="content"]').text
print(name,phoneNum,mailNum)
assert name=='cloudCcRaVTQWIxX' and phoneNum[-4:]=='6535' and mailNum=='未绑定'

注意 @pytest.mark.parametrize('homeurl,username,password',[('https://www.epubit.com/','*******6535','***')])不能直接运用再setup_class等方法上。

因为 setup_class 方法在测试类加载时调用,而参数化测试是在测试方法执行时进行的。

在Web应用的测试中,前置和后置函数通常用于执行以下操作。

  • 实例化/注销WebDriver
  • 设置页面为about:blank,最大化浏览器窗口或关闭浏览器窗口等
  • 导入/清理用户权限
  • 通过SQL初始化/清理对应页面的测试数据。

优化功能测试的物理组织结构

此处案例取自《Selenium自动化测试完全指南》第14章,建议查阅

仔细研究前面的案例测试,对一个获取个人信息功能写一个测试脚本,这样存在不少的问题:

  • 元素定位问题:元素定位代码遍布测试代码的各个位置,定位时使用的表达式晦涩难懂,阅读代码时,很难弄清楚它们到底是哪个页面上的哪一个元素,后期维护起来十分困难
  • 公共元素问题:虽然测试跨多个页面,但其中有一些关键元素可以公用,例如类别筛选下拉列表框。现在并未提取公共元素,如果以后下拉列表框的代码发生改变,就要满世界去寻找相关的代码。最糟糕的是,对于同一个元素,有些地方使用XPath定位,有些地方使用CSS选择器定位,还有些地方使用ID定位,根本看不出来那是同一个元素
  • 高度耦合问题:测试用例和Selenium WebDriver操作代码、Selenium元素定位代码、Selenium元素操作代码混杂在一起,耦合度极高,代码非常脆弱。

案例:使用异步社区的图书和文章分类功能,测试用例为“软件开发”“软件工程与方法”“软件测试与质量控制”,验证方式为结果的第一个图书的分类是否对的上。

按照我们刚学的知识,会通过写两个测试类完成自动化开发,这样就存在以上问题了,所以接下来我们对其结构进行修改。

解决元素定位问题——页面对象

在登录测试中,我们验证是否登录成功,采用的是登录的头像图片是否出现:

1
wd.find_element(By.XPATH,"//div[contains(@class,'userLogo')]/img").is_displayed()

对于此代码,我们最多能看出这是判断一个图片是否出现,但并没有说明这是登录头像,如果该元素的位置在网页中有改动,就需要在代码翻找此行代码,这在复杂庞大的测试中是很让人头疼的。

所以就有了一种方法,那就是创建页面对象,将页面的元素都放在一个页面对象中,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# homePage.py

from selenium.webdriver.common.by import By

class HomePage:
def __init__(self,webdriver):
self.wd = webdriver

# 用户头像
def userLogin_img(self):
return self.wd.find_element(By.XPATH,"//div[contains(@class,'userLogo')]/img")

def login_btn(self):
return self.wd.find_element(By.XPATH,"//i[text()='登录']")


#-------------------------
# 在功能测试代码中,我们就能写成
from homePage import HomePage
class TestEpubitCommon:
def test_epubit_login(self):
wd = webdriver.Chrome()
...
HomePage(wd).login_btn().click()
# wd.find_element(By.XPATH, "//i[text()='登录']").click()
...

# 验证
...
isShowUserImg = HomePage(wd).userLogin_img().is_displayed()
# isShowUserImg = wd.find_element(By.XPATH, "//div[contains(@class,'userLogo')]/img").is_displayed()
...
assert isShowExit and isShowUserImg and isJumpToHomePage
wd.quit()

HomePage(wd).userLogin_img().is_displayed()从这句代码来看,可读性大大提高,HomePage主页中的userLogin_img用户登录图片is_displayed是否显示,一目了然,比刚刚的XPath句法好读多了

并且如果之后userLogin_img用户登录图片有变动,也无需更改测试逻辑,只要在页面对象homePage中找到该元素的方法,对其进行更改即可,维护也更容易。

解决公共元素问题——继承

此处用到书中的例子。

首先在异步社区主页中,上边页眉在很多子页面中是复用的,前端代码也是如此,这就算公共元素,所以我们也可以在页面对象中通过继承进行复用。

image-20230824002346252

我们新建一个页眉对象文件,起名为siteHeader.py,那么homePage.py中的HomePage首页对象就可以继承SiteHeader页眉对象,其他的页面,例如图书界面BookPage中也存在公共的页眉,所以BookPage也能继承,这样就实现的代码的复用,维护时只要改变siteHeader.py,更加简洁了。

解耦测试工具

上面的优化已经解决了大部分维护问题,代码的健壮性足够。

但还有一点需要考虑的是,如果测试工具更新,甚至更换测试工具,我们的代码的更改会很困难,例如selenium把click()方法更新为c(),这需要逐行修改使用了click()的代码,如果有100个测试,将会耗费大量精力,所以此节将对测试工具selenium解耦

selenium中有webdriver.Chrome()这种工具级操作,还有find_element()这种元素级操作。

  • 工具级操作解耦

    在测试文件中,每个测试函数都存在以下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #创建并设置WebDriver实例
    driver = webdriver.Chrome()
    driver.implicitly_wait(5)
    driver.get(homeUrl)

    #切换窗体和创建显式等待的部分代码
    WebDriverWait(driver, 5).until_not(lambda d: articleListPage.filter_loadingMask())
    driver.switch_to.window(driver.window_handles[1])

    #注销WebDriver实例
    driver.quit()

    先创建测试工具操作类,代码文件为BaseLayer/executorBase.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    from selenium import webdriver
    from selenium.webdriver.support.wait import WebDriverWait
    from selenium.webdriver.common.by import By

    class ExecutorBase:
    def __init__(self, executor=None, url=None):
    if executor is None:
    self.__init_executor()
    else:
    self.driver = executor

    if url is not None:
    self.driver.get(url)

    #初始化测试执行器
    def __init_executor(self):
    #后期可以设置成从config文件读取或从命令行获取
    self.driver = webdriver.Chrome()
    self.driver.implicitly_wait(5)

    #获取测试执行器
    def get_executor(self):
    return self.driver

    #注销测试执行器
    def quit_executor(self):
    if self.driver is not None:
    self.driver.quit()
    self.driver = None

    #生成元素定位
    def __get_locator(self, key):
    if key.lower() == "xpath":
    return By.XPATH
    #...后续可以扩充其他分支定位,例如name/css等
    else:
    return By.ID

    #查找单个元素
    def get_element(self, key, value):
    return self.driver.find_element(self.__get_locator(key), value)

    #查找多个元素
    def get_elements(self, key, value):
    return self.driver.find_elements(self.__get_locator(key), value)

    #切换到最新打开的浏览器窗口
    def switch_to_last_window(self):
    lastWindowIndex = len(self.get_executor().window_handles) - 1
    self.get_executor().switch_to.window(self.get_executor().window_handles
    [lastWindowIndex])

    #等待元素消失
    def wait_for_element_disappear(self, get_element_func, seconds=5):
    WebDriverWait(self.get_executor(), seconds).until_not(lambda d: get_element_func())

    所有直接使用测试工具的函数都在ExecutorBase类中进行了封装。接下来只需要页面对象继承此基类,加以修改即可,例:

    1
    2
    3
    4
    5
    6
    7
    8
    class HomePage(ExecutorBase):

    # 用户头像
    def userLogin_img(self):
    return self.get_element(By.XPATH,"//div[contains(@class,'userLogo')]/img")

    def login_btn(self):
    return self.get_element(By.XPATH,"//i[text()='登录']")
  • 页面元素级操作解耦

    上面所说的click()操作就是页面元素级操作,解耦方式比较简单,先在ExecutorBase类中添加几个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class ExecutorBase:
    ...
    #单击元素
    def click_element(self, ele):
    ele.click()

    #获取元素文本
    def get_element_text(self, ele):
    return ele.text

    #获取元素集合的文本,并返回文本集合
    def get_elements_text_list(self, eles):
    text_list = []
    for ele in eles:
    text_list.append(ele.text)
    return text_list

    然后在页面对象中添加对应方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class HomePage(ExecutorBase):

    # 用户头像
    def userLogin_img(self):
    return self.get_element(By.XPATH,"//div[contains(@class,'userLogo')]/img")

    def click_userLogin_img(self):
    self.click_element(self.userLogin_img())
    ...

这样就完全解耦了,如果不再使用Selenium作为测试工具,或Selenium工具出现大幅的更新,只需修改最底层的BaseLayer/executorBase.py文件即可,界面操作代码和测试用例代码完全不受影响。

如果页面内容或结构发生变化,只需要修改PageObjects文件夹下对应页面的操作代码即可,测试工具代码和测试用例代码完全不受影响。

如果测试用例发生变化,只需要修改对应测试用例的test_xxx.py代码即可,界面操作的代码和测试用例的代码也完全不受影响。

流式编程——简化代码

举例:

1
2
3
4
assert  testPage.click_headerNavigation_to_bookListPage()\
.select_filter_category(filter1, filter2, filter3)\
.click_firstBook_and_switch_bookDetailPage()\
.get_summary_category_text() == filter3

具体实现只需要让各个页面对象类和公共区块类的操作代码在操作结束后返回页面对象实例即可。

1
2
3
4
5
6
7
8
9
10
11
12
class HomePage(ExecutorBase):

# 用户头像
def userLogin_img(self):
return self.get_element(By.XPATH,"//div[contains(@class,'userLogo')]/img")

def click_userLogin_img(self):
self.click_element(self.userLogin_img())
# 由于点击之后跳转到其他界面,所以需要导入其他界面对象
from PageObjects.***Page import ***Page
return ***Page(self.get_executor())
...

增加运行反馈机制

通过pytest-html、pytest-xdist、pytest-rerunfailures插件,可以生成测试报告,并行运行测试,以及在运行失败时重新执行测试。

pytest-html生成测试报告

1
2
3
4
5
6
# 安装
pip install pytest-html

# 命令
pytest -html=报告文件路径

并行运行测试

Pytest默认串行执行测试,对于普通的单元测试,这并无不妥。但对于Selenium测试,由于涉及页面操作,会产生很多网络开销,而且操作会很耗时。当测试较多时,这种串行执行测试的方式就不合时宜了。

1
2
3
4
5
6
7
# 安装
pip install pytest-xdist

# 命令
Pytest -v -n=4
#-n参数:将测试划分成4个进程
#-v参数:显示每个测试函数的执行结果,方便查看函数是在哪个进程运行

引入重试机制

出于各种各样原因,测试可能出现小概率运行失败的情况

例如网络阻塞,导致某个页面无法正常打开,测试就可能失败,但这并不意味着被测试程序真的出现问题了

为了避免偶发性失败,可以引入重试机制。如果重试多次后依然失败,那就可以断定测试失败可能并非偶然的,而是必然的失败,并开始人工介入。

1
2
3
4
5
6
# 安装
pip install pytest-rerunfailures

# 命令
pytest --reruns=重试次数
# 加上--html,可在报告中 查看是否发生了return