Python单元测试框架pytest
前提
pytest是一个非官方的单元测试框架,需要先进行安装。所以pip一下
技术点
一、运行参数(进入到相应目录)
1、无参数运行
pytest运行命名规则:运行时查找当前目录下及子目录下的以 test_*.py 或者 *_test.py为标识的文件,文件中的内容只运行test开头的函数或类
运行目录下的所有py文件:pytest
运行目录下某一个py文件:pytest test_01.py
运行目录下py文件中的某个类:pytest test_02.py::TestClass
运行目录下py文件中某个类的某个方法:pytest test_02.py::TestClass::test_one
指定目录运行:pytest testpy
2、-v参数
打印详细的日志信息
3、-s参数
代码里面有 print 输出语句,想在运行结果中打印 print 输出
-v 和-s 可以写在一块 -sv
4、-k参数
pytest -k '类名' //运行指定的类
pytest -k '方法名' //运行指定的方法
pytest -k '类名 and not 方法名' //运行类里的方法,不包含某个方法
pytest -k “test_demo1 or test_demo2” //执行test_demo1 和 test_demo2
5、-x参数
遇到失败用例立即停止运行
6、--maxfail参数
用例失败个数达到阀值后停止运行
pytest --maxfail 2 test_02.py
7、-m参数 分组
只运行标记 @pytest.mark.[标记名] 的方法和类
比如类名上添加:@pytest.mark.oneone,
执行命令:pytest -m "oneone" test_02.py
如果有多个标记可以用 and 或 or 进行组合
1 import pytest 2 3 def inc(x): 4 return x + 1 5 6 class TestPytest: 7 def setup(self): 8 print("setup") 9 10 @pytest.mark.success 11 def test_demo_1(self): 12 assert inc(4) == 5 13 14 @pytest.mark.fail 15 def test_demo_2(self): 16 assert inc(3) == 5 17 18 def teardown(self): 19 print("teardown")
1 仅执行标记为success的用例,以下两种写法均可以: 2 pytest -m "success and not fail" test_pytest.py 3 pytest -m "success" test_pytest.py
8、--durations参数
获取执行最慢的一个:pytest --durations=1
9、--collect-only参数
只收集用例不执行,可用于统计用例数
10、-q参数
只显示用例执行结果
二、pytest前置和后置
1 import pytest 2 3 def setup_module(): 4 print("\nsetup_module,一个py文件只执行一次,不管该py文件中有多少方法和类") 5 def teardown_module(): 6 print("\nteardown_module,一个py文件只执行一次,不管该py文件中有多少方法和类") 7 8 9 # setup_function teardown_function作用于 类外 的函数 10 def setup_function(): 11 print("\nsetup_function") 12 def teardown_function(): 13 print("\nsetup_function") 14 15 def test_cls_out(): 16 print("类外的函数方法") 17 18 19 class TestPytest1: 20 def setup_class(self): 21 print("\nsetup_class1,在类中只执行一次,当有多个测试方法的时候使用") 22 def teardown_class(self): 23 print("\nteardown_class1,在类中只执行一次,当有多个测试方法的时候使用") 24 25 # 等同于下边的setup、teardown 26 def setup_method(self): 27 print("\nsetup_method1,在类中的每个测试方法都执行一次") 28 def teardown_method(self): 29 print("teardown_method1,在类中的每个测试方法都执行一次") 30 31 # 等同于上边的setup_method、teardown_method 32 def setup(self): 33 print("setup") 34 def teardown(self): 35 print("teardown") 36 37 38 def test_three(self): 39 print("test_three,测试用例") 40 41 def test_four(self): 42 print("test_four,测试用例")
三、控制执行顺序
安装:pip install pytest-ordering
负数越小越先执行(-100,-18,-1)
正数越小越先执行(1,18,100)
1 import pytest 2 3 4 class Testpy: 5 @pytest.mark.run(order=1) 6 def test_one(self): 7 print(111) 8 9 @pytest.mark.run(order=18) 10 def test_two(self): 11 print(222) 12 13 @pytest.mark.run(order=100) 14 def test_three(self): 15 print(333)
四、并发执行
安装:pip install pytest-xdist
多个CPU并行执行用例,如果参数为 auto 自动检测系统的 CPU 数目;如果参数为数字,则指定运行测试的处理器进程数。
pytest -n auto
pytest -n [num]
分布式执行用例的设计原则: 1、用例之间是独立的,用例之间没有依赖关系,用例可以完全独立运行 2、用例执行没有顺序,随机都可以正常执行 3、每个用例都能重复运行,运行结果不会影响其他用例
五、pytest-html 生成测试报告
安装:pip install pytest-html
指定报告的存放路径
--html=./report/report.html
加这个参数生成的报告css不是独立的
--self-contained-html
#test_pytest.py
1 import pytest 2 3 def inc(x): 4 return x + 1 5 6 class TestPytest: 7 def setup(self): 8 print("setup") 9 10 @pytest.mark.parametrize("data,expected", [(2, 3), (6, 7), (8, 10)]) 11 def test_demo_1(self, data, expected): 12 assert inc(data) == expected 13 14 def teardown(self): 15 print("teardown")
终端执行命令:pytest test_pytest.py --html=report.html --self-contained-html
会在当前目录下生成html报告,如下:
六、assert断言
assert a
assert a == b
assert a in b
assert not a
assert a != b (a <> b一般不再使用)
七、@pytest.fixture
fixture 有一个参数 scope,通过 scope 可以控制 fixture 的作用范围,根据作用范围大小划分:session> module> class> function
具体作用范围如下:
function 每个函数或者方法级别都会被调用(默认)
class 每个类级别调用一次
module 每个.py文件【模块级别】调用一次
session 多个文件调用一次(可以跨.py文件调用,每个.py文件就是module)
1、以参数的形式传入到方法里执行
import pytest @pytest.fixture() def login(): text = "登录需要的操作步骤" return text @pytest.fixture() def operate(): text = "用例的执行步骤" return text @pytest.fixture() def xiao(): text = "1234567890" return text # 需要在函数中传入函数名,函数上需要先标记上 @pytest.fixture() def test_case1(login, operate, xiao): #在入参中 传入方法名 即可实现调用已装饰的fixture下函数 print(f"需要先进行登录:{login},然后执行用例:{operate},最后执行:{xiao}") print("test_case1,需要登录执行完毕")
2、fixture的两种引用方式
1 import pytest 2 3 4 @pytest.fixture() 5 def open(): 6 print("打开浏览器") 7 yield 8 9 print("执行teardown !") 10 print("最后关闭浏览器") 11 12 13 # 方式一:装饰器方式引用 16 @pytest.mark.usefixtures("open") 17 def test_search1(): 18 print("test_search1") 19 # raise NameError 20 pass 21 22 23 # 方式二:在函数中传入 26 def test_search2(open): 27 print("test_search2") 28 # raise NameError 29 pass
3、conftest.py应用
conftest.py等同于scope=session
pytest test_scope1.py test_scope2.py
相应代码见:https://github.com/hanshoukai/pytest_fixture
fixture为function
fixture为class
fixture为module
fixture为session
yield及调用执行流程
- yield前的代码为前置执行逻辑
- yield为分界线,后面的代码为用例结束后执行的代码逻辑
- 调用方式都可以直接将方法名open当做参数传入
- 调用执行流程:运行测试用例--发现参数为方法名--寻找同名的函数方法--先执行同名函数方法--碰到yield--执行测试用例--执行yield后续代码
- conftest.py无异常,测试用例发生异常时,不会影响到yield的执行,会在异常结束后继续执行yield后的代码
- conftest.py yield前存在异常,测试用例调用后续yield的代码,不会再继续执行
4、自动执行fixture装饰下的函数
conftest.py中标记为:@pytest.fixture(autouse="true")
每个测试函数都会自动调用该前置函数
5、fixture传递参数
import pytest @pytest.fixture(params=[1, 2, 3]) # 函数需要传入request【固定写法】 def params(request): # 通过request.param访问每个参数的值 return request.param def test_com(params): print(f"测试数据:{params}") assert params < 5
八、@pytest.mark.parametrize参数化
传一个参数
1 # 传一个参数 2 @pytest.mark.parametrize("user", ["13552977251", "13552554252"]) 3 def test_user(user): 4 print(user)
传两个参数
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+5", 7), ("7*5", 35)])
如上 "test_input,expected" 可以修改为 列表或者元组 的形式
列表:@pytest.mark.parametrize(["test_input","expected"], [("3+5", 8), ("2+5", 7), ("7*5", 35)])
元组:@pytest.mark.parametrize(("test_input","expected"), [("3+5", 8), ("2+5", 7), ("7*5", 35)])
1 # 传两个参数 2 @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+5", 7), ("7*5", 35)]) 3 def test_function(test_input, expected): 4 # eval可以将字符串转成3+5的表达方式 5 print(test_input, expected) 6 assert eval(test_input) == expected
传三个参数 三组数据,ids是别名必须与参数的数量保持一致
参数组合
1 # 参数组合 2 @pytest.mark.parametrize("x", [1, 2]) 3 @pytest.mark.parametrize("y", [3, 4, 5]) 4 def test_num(x, y): 5 # print(f"测试数据组合x: {x} , y:{y}") 6 print("测试数据组合x:"+str(x)+" y:"+str(y))
函数返回值类型
1 # 函数返回值类型 2 def return_data(): 3 return [(1, 2), (3, 4)] 4 5 @pytest.mark.parametrize("a,b", return_data()) 6 def test_data(a, b): 7 print(a) 8 print(b)
用yaml文件做为参数化的数据源
companyid.yaml
- - 23725503 - 24721214 - 2352987806
data.yaml
- - 20 - 30 - - 40 - 50
脚本:
1 import pytest 2 import yaml 3 4 5 @pytest.mark.parametrize('a, b', yaml.safe_load(open("data.yaml", encoding='utf-8'))) 6 # @allure.step("方法的描述信息") 7 def test_fo(a, b): 8 print(a) # 【20 - 30】 9 print(b) # 【40 - 50】 10 11 12 @pytest.mark.parametrize('company_id', yaml.safe_load(open("companyid.yaml", encoding='utf-8'))) 13 # @allure.step("方法的描述信息") 14 def test_foo(company_id): 15 print("企业ID:", company_id) # 企业ID: [23725503, 24721214, 2352987806]
九、pytest.ini模板
1 [pytest] 2 # 空格分隔,可添加多个命令行参数 重试两次每隔5秒 生成报告 3 addopts = -sv --reruns 2 --reruns-delay 5 --html=./report/report.html --self-contained-html 4 #addopts = -s -v --alluredir=./result 5 # 当前目录下 6 testpaths = ./testcase/ 7 #配置测试搜索的文件名称,当前目录下以test开头,以.py结尾的所有文件 8 python_files = test*.py 9 #配置测试搜索的测试类名,当前目录下以Test开头的所有类 10 python_classes = Test 11 #配置测试搜索的测试类名,当前目录下以test_开头的所有方法 12 python_functions = test_*
十、失败重试
用例失败后自动重新运行:pip install pytest-rerunfailures
使用方法:pytest test_x.py --reruns=2 --reruns-delay 5 #失败后重试2次,每次间隔5秒
在脚本中指定定义重跑的次数,这个时候在运行的时候,就无需加上 --reruns 这个参数
1 @pytest.mark.flaky(reruns=6, reruns_delay=2) 2 def test_example(self): 3 print(3)
十一、多重校验
单个测试用例中执行多个断言,并查看所有的失败信息,从而更好地理解测试结果
安装:pip install pytest-assume
import pytest def test_simple_assume(): pytest.assume(1==1) pytest.assume(1!=2) pytest.assume(1!=3)