Pytest测试框架
pytest测试框架
基本介绍
- 介绍:pytest是python的一种单元测试框架,同自带的Unittest测试框架类似,相比于Unittest框架使用起来更简洁,效率更高。
- 特点
- 易上手,入门简单
- 支持简单单元测试和复杂的功能测试
- 支持参数化
- 执行测试过程中可以将某些测试跳过,或者对某些预期失败的case标记成失败
- 支持重复执行失败的case
- 支持运行由Nose,Unittest编写的测试Case
- 具有第三方插件,可以自定义扩展
- 方便和持续集成工具集成
pytest的安装方式
- linux下:sudo pip3 install -U pytest
- windows下:pip3 install -U pytest
- 运行pytest --version会展示当前已安装版本
pytest的设计原则
- 文件名以test_*py的格式
- 以test_开头的函数
- 以Test开头的类
- 以test_开头的类方法
- 所有包必须要有__init__.py文件
批注:为什么每个包都必须要带有__init__.py文件?
理由:一个包是一个带有特殊文件__init__.py文件定义了包的属性和方法.其实它可以什么都不定义,可以是一个空文件,但是必须存在.如果__init__.py不存在,这个目录就仅仅是一个目录,而不是一个包,它就不能被导入或者包含其他的模块和嵌套包.
pytest的常用断言
- assert A: 判断A是真
- assert not A: 判断A不为真
- assert A in B: 判断B包含A
- assert A==B: 判断A等于B
- assert A!=B: 判断A不等于B
pytest的执行方法
- pytest
- py.test
- python -m pytest
- 执行目录下的所有用例 pytest 文件名/
- 执行某个文件下用例 pytest 脚本名.py
- 运行模块里面的某个函数或者某个类(加-v打印的信息更详细) pytest -v 文件名.py::TestClass::test_method
- 标记式表达式(将会运行用@ pytest.mark.slow装饰器修饰的所有测试) pytest -m slow
- 简单打印(只打印结果) pytest -q 文件名.py
- 详细打印 pytest -q 文件名.py
- 遇到错误停止测试 pytest -x test_class.py
- 错误达到指定数量停止测试 pytest --maxfail=1
- 执行用例包含http的用例 pytest -s -k http start.py
- 根据用例排除某些用例 pytest -s -k "not http" start.py
- 同时匹配不同的测试用例 pytest -s -k "method or weibo" start.py
pytest的用例运行级别
- 模块级别 setup_module、teardown_module
- 函数级别 setup_function、teardown_function,不在类中的方法
- 类级别 setup_class、teardown_class
- 方法级别 setup_method、teardown_method
- 方法细化级别 setup、teardown
pytest之fixture
从模块级别中来看,用例加setup和teardown可以实现在测试用例之前或之后加入一些操作,但这种是整个脚本全局生效的,如果想实现以下场景:用例1需要先登录,用例2不需要登录,用例3需要先登录,那么很显然使用setup和teardown就实现不了,于是可以自动以测试用例的预置条件
fixture的优势
fixture相对于setup和teardown来说有以下几点优势:
- 命名方式灵活,不限于setup和teardown这几个命名
- conftest.py配置里可以实现数据共享,不需要import就能自动找到一些配置
- scope="module"可以时间多个.py跨文件共享前置,每一个.py文件调用一次
- scope="session"以实现多个.py跨文件使用一个session来完成多个用例
fixture参数列表
使用@pytest.fixture(scope="function",params=None,autouse=False,ids=None,name=None):装饰器来标记fuxture的功能,具体参数详解如下:
- scope 四个级别参数
- 默认的function 每一个函数或方法都会调用
- class 每一个类调用一次
- module 作用是当前整个py文件都能生效,参数写上函数名称即可
- session 多个文件调用一次,可以跨.py文件调用,每个.py文件就是module
- params 一个可选的参数列表,会导致多个参数fixture功能和所有测试使用它
- autouse 默认:False,需要用例手动调用该fixture;如果是True,所有作用域内的测试用例都会自动调用该fixture
- ids 每个字符串id的列表,每个字符串对应与params这样他们就是测试ID的一部分,如果没有提供ID它们将从params自动生成
- name 默认:装饰器的名称,同一模块的fixture相互调用建议写个不同的name
fixture的调用
- 将fixture名称作为测试用例函数的输入参数(如果不指定fixture名称,那么将函数明作为参数)
- 测试用例加上装饰器:@pytest.mark.usefixtures(fixture_name)
- fixture设置autouse=True
import pytest
class Test_login:
# 调用方式1(使用函数名作为参数传递)
@pytest.fixture
def login(self):
print("我叫panda")
def test_result(self, login):
print("我今年20岁")
# 调用方式1(使用fixture名称作为参数传递)
@pytest.fixture(name="name")
def login1(self):
print("我叫sunmmer")
def test_result1(self, name):
print("我今年21岁")
# 调用方式2(使用usefixtures)
@pytest.mark.usefixtures("login")
def test_result2(self):
print("我今年22岁")
"""
# 调用方式3(使用autouse),每个测试用例都会调用
@pytest.fixture(autouse=True)
def login(self):
print("我叫sunfree")
def test_result3(self):
print("我今年23岁")
"""
if __name__ == '__main__':
pytest.main(["-s", "fixture.py"])
注意点:
- 在类声明上面加@pytest.mark.usefixtures(),代表的是这个类里面所有测试用例都会调用该fixture
- 可以叠加多个 @pytest.mark.usefixtures() ,先执行的放底层,后执行的放上层
- 可以传多个fixture参数,先执行的放前面,后执行的放后面
- 如果fixture有返回值,用 @pytest.mark.usefixtures() 是无法获取到返回值的,必须用传参的方式(方式一)
- 添加了 @pytest.fixture ,如果fixture还想依赖其他fixture,需要用函数传参的方式,不能用 @pytest.mark.usefixtures() 的方式,否则会不生效
- scope使用module只会在第一个用例之前执行一次,如果我第一个测试用例之前不调用,那么我就在第二个测试用例调用open函数
scope+yield
# 新建test_demo.py
import pytest
@pytest.fixture(scope="module")
def open():
print("打开浏览器,访问至百度首页")
yield # 编写用例时,将yield写在结束操作之前就可,然后在所有用例执行完之后执行一次
print("这是teardown操作")
print("关闭浏览器")
def test_case1(open):
print("用例1")
def test_case2(open):
print("用例2")
if __name__ == "__main__":
pytest.main(["-v", "test_demo.py"])
# 结果:上面的用例都调用了open()
# 操作,在所有用例执行前执行一次open,然后运行用例,最后所有用例执行完之后执行一次yield后面的结束操作
# 注:yield在用例里面充当了teardown操作。就算用例执行报错,yield还是会正常执行不会被影响
注意点:
- 如果yield前面的代码,即setup部分已经抛出异常了,则不会执行yield后面的teardown内容
- 如果测试用例抛出异常,yield后面的teardown内容还是会正常执行
终结函数
from selenium import webdriver
from page.page_login import PageLogin
import page
import time
import pytest
@pytest.fixture(scope="session")
def driver(request):
driver = webdriver.Firefox()
driver.maximize_window()
driver.get(page.url)
def close():
time.sleep(3)
driver.quit()
request.addfinalizer(close)
return driver
注意点:
- 如果 request.addfinalizer() 前面的代码,即setup部分已经抛出异常了,则不会执行 request.addfinalizer() 的teardown内容(和yield相似,应该是最近新版本改成一致了)
- 可以声明多个终结函数并调用
测试结果状态
用例执行完成后,每条用例都有自己的状态,常见的状态有
- passed:测试通过
- failed:断言失败 测试用例的代码有一场,包括主动抛出异常或代码有一场,都算failed
- error:用例本身写的质量不行,本身代码报错(譬如:fixture不存在,fixture里面有报错)
- xfail:预期失败,加了 @pytest.mark.xfail()
pytest之conftest.py配置文件
在上面的案例中,同一个py文件,多个用例调用一个登陆功能,如果有多个py文件都需要调用这个登陆功能的话,就不能把登录写到用例里面去了,此时应该有个配置文件,单独管理一些预置的操作场景,pytest里面默认读取conftest.py里面的配置
conftest.py配置需要注意一下几点:
- conftest.py配置名称是固定的,不能改名称
- conftest.py与运行的用例要在同一个package下,并且有__init__.py文件
- 不需要import导入conftest.py,pytest用例会自动查找
from selenium import webdriver
import page
import time
import pytest
@pytest.fixture(scope="session")
def driver(request):
driver = webdriver.Firefox()
driver.maximize_window()
driver.get(page.url)
def close():
time.sleep(3)
driver.quit()
request.addfinalizer(close)
return driver
pytest之skip、skipif
- pytest.mark.skip可以标记无法在某些平台上运行的测试功能,或者您希望失败的测试功能
- 希望满足某些条件才执行某些测试用例,否则pytest会跳过运行该测试用例
- 实际常见场景:跳过非Windows平台上的仅Windows测试,或者跳过依赖于当前不可用的外部资源(例如数据库)的测试
@pytest.mark.skip()
跳过执行测试用例,可选参数reason:表示跳过的原因
import pytest
class TestLogin:
@pytest.fixture
def login(self):
print("我叫panda")
def test_result(self, login):
print("我今年20岁")
@pytest.mark.skip(reason="就是不想执行")
def test_splice(self):
print("不想进行拼接")
if __name__ == '__main__':
pytest.main(["-s", "fixture.py"])
注意点:
- @pytest.mark.skip 可以加在函数上,类上,类方法上
- 如果加在类上面,类里面的所有测试用例都不会执行
- 以上都是针对:整个测试用例方法跳过执行
pytest.skip()
在测试用例执行期间强制跳过不在执行剩余内容
import pytest
class TestLogin:
def test_one(self):
print("test_one方法执行")
if 1 == 1:
pytest.skip("skip") # 如果if语句为True时,执行到这里会跳过,方法后面的代码都不会执行
assert 1 == 1
def test_two(self):
print("test_two方法执行")
assert 'o' in 'love'
def test_three(self):
print("test_three方法执行")
assert 3 - 2 == 1
if __name__ == '__main__':
pytest.main(["-s", "fixture.py"])
@pytest.mark.skipif()
希望有条件的跳过某些测试用例
import pytest
class TestClass:
name = 'one'
@pytest.mark.skipif(name == 'one', reason="skip") # 如果装饰器里面的条件满足会跳过改方法
def test_one(self):
print("test_one方法执行")
assert 1 == 1
def test_two(self):
print("test_two方法执行")
assert 'o' in 'love'
if __name__ == '__main__':
pytest.main(["-sv", "fixture.py"])
标记变量
- 可以将 pytest.mark.skip 和 pytest.mark.skipif 赋值给一个标记变量
- 在不同模块之间共享这个标记变量
- 若有多个模块的测试用例需要用到相同的 skip 或 skipif ,可以用一个单独的文件去管理这些通用标记,然后适用于整个测试用例集
import pytest
class TestClass:
name = 'one'
skipif=pytest.mark.skipif(name == 'one', reason="skip")
@skipif # 如果装饰器里面的条件满足会跳过改方法
def test_one(self):
print("test_one方法执行")
assert 1 == 1
def test_two(self):
print("test_two方法执行")
assert 'o' in 'love'
if __name__ == '__main__':
pytest.main(["-sv", "fixture.py"])
pytest的参数化
常规操作实例
解决测试重复操作,不同数据的情况
import pytest
class TestClass:
# 以下参数化中
# 参数名称可以是列表套字符串,元祖套字符串,也可以字符串套字符(字符逗号隔开)
# 参数值可以是列表套列表,也可以列表套元祖
# @pytest.mark.parametrize(["a","b","c"], [(1, 2, 3), (2, 3, 5), (3, 4, 7)])
# @pytest.mark.parametrize(("a", "b", "c"), [(1, 2, 3), (2, 3, 5), (3, 4, 7)])
# @pytest.mark.parametrize("a,b,c", [(1, 2, 3), (2, 3, 5), (3, 4, 7)])
@pytest.mark.parametrize("a,b,c", [[1,2,3],[2,3,5]])
def test_sum(self, a, b, c):
assert a + b == c
if __name__ == '__main__':
pytest.main(["-s", "fixture.py"])
# 实际的web UI自动化的开发场景,登录框
1. 需要测试账号空、密码空、账号密码都为空、账号不存在等情况
2. 这些用例的区别就在于输入的测试数据和对应的交互结果
3. 所以只需要写一条登录测试用例,然后把多组测试数据和期望结果参数化,节省很多代码量
源码分析
def parametrize(self,argnames, argvalues, indirect=False, ids=None, scope=None)\
- argnames 参数名字
- 存放字符串,以逗号隔开
- 存放list
- 存放dict
- ids 用例的ID,传入一个字符串列表,用来标识每一个测试用例
- indirect 如果设置成True,则把传入的参数当做函数执行,而不仅仅指的是参数
结合fixture
如果想把登录操作放到前置操作里,也就是用到@pytest.fixture装饰器,传参就用默认的request参数
request传入一个参数
# test_02.py
# coding:utf-8
import pytest
# 测试账号数据
test_user_data = ["admin1", "admin2"]
@pytest.fixture(scope="module")
def login(request):
user = request.param
return user
@pytest.mark.parametrize("login", test_user_data, indirect=True)
#添加indirect=True参数是为了把login当成一个函数去执行,而不是一个参数
def test_login(login):
'''登录用例'''
a = login
print("测试用例中login的返回值:%s" % a)
assert a != ""
if __name__ == "__main__":
pytest.main(["-s", "test_02.py"])
reuquest传入两个参数
如果用到@pytest.fixture里面用2个参数情况,可以把多个参数用一个字典去存储,这样最终还是只传一个参数不同的参数再从字典里面取对应key值就行,如: user = request.param["user"]
# test_03.py
# coding:utf-8
import pytest
# 测试账号数据
test_user_data = [{"user": "admin1", "psw": "111111"},
{"user": "admin1", "psw": ""}]
@pytest.fixture(scope="module")
def login(request):
user = request.param["user"]
psw = request.param["psw"]
print("登录账户:%s" % user)
print("登录密码:%s" % psw)
if psw:
return True
else:
return False
# indirect=True 声明login是个函数
@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
'''登录用例'''
a = login
print("测试用例中login的返回值:%s" % a)
assert a, "失败原因:密码为空"
if __name__ == "__main__":
pytest.main(["-s", "sdfdsf.py"])
多个fixture
用例上面可以同时放多个fixture,也就是多个前置操作,可以支持装饰器叠加,使用parametrize装饰器叠加时,用例组合是2个参数个数相乘
# test_04.py
# coding:utf-8
import pytest
# 测试账号数据
test_user = ["admin1", "admin2"]
test_psw = ["11111", "22222"]
@pytest.fixture(scope="module")
def input_user(request):
user = request.param
print("登录账户:%s" % user)
return user
@pytest.fixture(scope="module")
def input_psw(request):
psw = request.param
print("登录密码:%s" % psw)
return psw
@pytest.mark.parametrize("input_user", test_user, indirect=True)
@pytest.mark.parametrize("input_psw", test_psw, indirect=True)
def test_login(input_user, input_psw):
'''登录用例'''
a = input_user
b = input_psw
print("测试数据a-> %s, b-> %s" % (a, b))
assert b
if __name__ == "__main__":
pytest.main(["-s", "test_04.py"])
pytest之pytest-rerunfailures
- 安装 pip install pytest-rerunfailures
- 执行方法(重新运行与等待时间可以结合使用)
- 命令行参数 --reruns n(重新运行次数),--reruns-delay m(等待运行秒数)
- 装饰器参数 reruns=n(重新运行次数),reruns_delay=m(等待运行秒数)
- 相关操作
- 重新运行所有失败的用例 pytest --reruns 5 -s
- 重试之间增加延迟时间 pytest --reruns 5 --reruns-delay 10 -s
- 重新运行指定测试用例 单个测试用例添加flaky装饰器 @pytest.mark.flaky(reruns=5),在测试失败时自动重新运行
- 重新运行并增加延迟时间 @pytest.mark.flaky(reruns=5, reruns_delay=2)
pytest的测试报告
- 安装插件 pip install pytest-html
- 执行方法 pytest --html=report.html
- 指定路径执行 pytest --html=./report/report.html
- 将--html=report/report.html追加到pytest.ini的addopts后
- 报告独立显示,产生的报告css是独立的,分享报告的时候样式会丢失,为了更好的分享发邮件展示报告,可以把css样式合并到html里 pytest --html=report.html --self-contained-html
pytest之pytest-repeat
自动化运行用例时候,也会出现偶然的bug,可以针对单个用例,或者针对某个模块的用例重复执行多次
- 安装插件 pip install pytest-repeat
- 重复测试直至失败,将pytest的 -x 选项与pytest-repeat结合使用 py.test --count=1000 -x test_file.py
- 在代码中将某些测试用例标记为执行重复多次,可以使用 @pytest.mark.repeat(count)
- 覆盖默认的测试用例执行顺序,类似fixture的scope参数 --repeat-scope 例:pytest -s --count=2 --repeat-scope=class repeat.py
- function:默认,范围针对每个用例重复执行,再执行下一个用例
- class:以class为用例集合单位,重复执行class里面的用例,再执行下一个
- module:以模块为单位,重复执行模块里面的用例,再执行下一个
- session:重复整个测试会话,即所有测试用例的执行一次,然后再执行第二次
pytest之pytest.ini
pytest的配置文件通常放到测试目录下,名称为pytest.ini,命令行运行时会使用该配置文件中的配置,在开头添加[pytest],添加剩余的内容如下:
- 配置pytest命令行运行参数 addopts=-s
- 配置测试搜索的路径 testpaths=./scripts
- 配置测试搜索的文件名 python_files=test_*.py
- 配置测试搜索的测试类名 python_classes=Test*
相关代码:
[pytest]
# 添加命令行参数 添加生成报告快捷命令
addopts = -s --html=report/report.html
# 搜索哪个文件夹
testpaths = ./scripts
# 函数名
python_functions=test_*
# 文件名
python_files = test_*.py
# 类名
python_classes = Test*
pytest之pytest-ordering
函数修饰符的方式标记被测函数执行的顺序
安装方式:
插件名称:使用命令行 pip3 install pytest-ordering
使用方法:
标记于被测试函数,@pytest.mark.run(order=x)
order的优先级 0>较小的正数>较大的正数>无标记>较小的负数>较大的负数
根据order传入的参数来结局运行顺序
order值全为证书或权威负数时,运行顺序:值越小,优先级越高
正数和负数同时存在:正数优先级高
默认情况下,pytest默认从上往下执行,可以通过第三方插件包改变其运行顺序.
pytest之@pytest.mark.xfail
当用例a失败的时候,如果用例b和用例c都是依赖于第一个用例的结果,那可以直接跳过用例b和c的测试,直接给他标记失败xfail用到的场景,登录是第一个用例,登录之后的操作b是第二个用例,登录之后操作c是第三个用例,很明显三个用例都会走到登录,如果登录都失败,那后面个用例就没有测试的必要了,并且标记为失败用例
标记测试函数为失败函数
方法:xfail(condition=None,reason=None,raises=None,run=true,strict=False)
常用参数:
condition:预期失败的条件,必传参数 如果传True表示确认设定预期失败,传False表示设定预定成功
reason:失败的原因,必传参数 reason="done",可以传任何参数值,我们设定为done
使用方法:@pytest.mark.xfail(condition,reason="xx")
pytest之mark标记
pytest可以支持自动以标记,自动以标记可以把一个web项目划分为多个模块,然后指定模块名称执行,一个大项目自动化用例时,可以划分多个模块,也可以使用标记功能,表明哪些是模块1,哪些是模块2,运行代码时指定mark名称运行就可以
以下用例,标记test_send_http()为webtest
# content of test_server.py
import pytest
@pytest.mark.webtest
def test_send_http():
pass # perform some webtest test for your app
def test_something_quick():
pass
def test_another():
pass
class TestClass:
def test_method(self):
pass
if __name__ == "__main__":
pytest.main(["-s", "test_server.py", "-m=webtest"])
只运行用webtest标记的测试,在运行的时候,加个-m参数,指定参数值webtest pytest -v -m webtest
如果不想执行标记webtest的用例,那就用"not webtest" pytest -v -m "not webtest"
如果想指定运行某个.py模块下,类里面的一个用例,如:TestClass里面test_method用例,每个test_开头(或_test结尾)的用例,函数(或方法)的名称就是用例的节点id,指定节点id运行用-v 参数,当然也可以选择运行整个class pytest -v test_server.py::TestClass
if __name__ == "__main__":
pytest.main(["-v", "test_server.py::TestClass::test_method"])
if __name__ == "__main__":
pytest.main(["-v", "test_server.py::TestClass", "test_server.py::test_send_http"])
pytest之运行上次失败用例
80%的bug集中在20%的模块,越是容易出现bug的模块,bug是越改越多,当开发修复完bug后,一般是重点测上次失败的用例,那么自动化测试也是一样,所以为了节省时间,可以值测试上次失败的用例
- pytest --lf 只重新运行上次运行失败的用例(或如果没有失败的话会全部跑)
- pytest --ff 运行所有测试,但首选运行上次运行失败的测试(这可能会重新测试,从而导致重复的fixture setup/teardown)
pytest之pytest-xdist
优点:节约时间
安装插件:pip install pytest-xdist
并行测试
直接加个-n参数即可,后面num参数就是并行数量,比如num设置为3 pytest -n 3
使用pytest-xdist插件也能生成html报告,完美支持pytest-html插件 pytest -n 3 --html=report.html --self-contained-html
分布式测试(使用nginx实现)
知识盲区,后续补充...
pytest之pytest-assume
pytest断言失败后,后面的代码就不会执行了,通常一个用例会写多个断言,有时候我们希望第一个断言失败后,后面能继续断言,那么pytest-assume插件可以解决断言失败后继续断言的问题
安装 pip install pytest-assume
断言案例
import pytest
@pytest.mark.parametrize(('x', 'y'),
[(1, 1), (1, 0), (0, 1)])
def test_simple_assume(x, y):
print("测试数据x=%s, y=%s" % (x, y))
pytest.assume(x == y)
pytest.assume(x + y > 1)
pytest.assume(x > 1)
print("测试完成!")
上下文管理器
pytest.assume也可以使用上下文管理器去断言,需要注意的是每个with块只能有一个断言,如果一个with下有多个断言,当第一个断言失败的时候,后面的断言就不会起作用
import pytest
from pytest import assume
@pytest.mark.parametrize(('x', 'y'),
[(1, 1), (1, 0), (0, 1)])
def test_simple_assume(x, y):
print("测试数据x=%s, y=%s" % (x, y))
with assume: assert x == y
with assume: assert x + y == 1
with assume: assert x == 1
print("测试完成!")