文章目录
一、Pytest安装
pytest是python的一种单元测试框架,与python自带的unittest测试框架类似,但是比unittest框架使用起来更简洁,效率更高。根据pytest的官方网站介绍,它具有如下特点:
- 非常容易上手,入门简单,文档丰富,文档中有很多实例可以参考
- 能够支持简单的单元测试和复杂的功能测试
- 支持参数化
- 执行测试过程中可以将某些测试跳过(skip),或者对某些预期失败的case标记成失败
- 支持重复执行(rerun)失败的case
- 支持运行由nose, unittest编写的测试case
- 可生成html报告
- 方便的和持续集成工具jenkins集成
- 可支持执行部分用例
- 具有很多第三方插件,并且可以自定义扩展
为什么要学习这个框架,看python鄙视链:pytest 鄙视 > unittest 鄙视 > robotframework 鄙视 > 记流水账 鄙视 > "hello world"小白
# 安装
pip install -U pytest
# 查看版本
pip show pytest / pytest --version
二、Pytest用例规则和运行
1)测试用例命名和编写规则
-
测试文件以test_开头(以_test结尾也可以)
-
测试类以Test开头,并且不能带有
__init__
方法 -
测试函数以test_开头
-
断言使用assert
2)运行测试用例
1.执行某个目录下所有的用例
pytest 文件名/
2.执行某一个py文件下用例
pytest 脚本名称.py
3.-k 按关键字匹配
pytest -k "My TestCase"
这将运行包含与给定字符串表达式匹配的名称的测试,其中包括Python
使用文件名,类名和函数名作为变量的运算符。
4.按节点运行
每个收集的测试都分配了一个唯一的nodeid,它由模块文件名和后跟说明符组成
来自参数化的类名,函数名和参数,由:: characters分隔。
运行.py模块里面的某个函数
pytest test_mod.py::test_func
运行.py模块里面,测试类里面的某个方法
pytest test_mod.py::TestClass::test_method
5.标记表达式
pytest -m smoke
将运行用@pytest.mark.smoke装饰器修饰的所有测试。
import pytest
@pytest.mark.smoke
def test_demo():
pass
6.从包里面运行
pytest --pyargs pkg.testing
这将导入pkg.testing并使用其文件系统位置来查找和运行测试。
7.-x 遇到戳我时停止测试
pytest -x test_class.py
8、-s 关闭捕捉,输出打印信息。
pytest -s test_demo.py
9、-q 安静模式, 不输出环境信息
pytest -q test_demo.py
10、-v:丰富信息模式,输出更详细的用例执行信息
pytest -v test_demo.py
11、-lf:是–last-failed的缩写, 重新运行上次运行失败的用例(或如果没有失败的话会全部跑)
pytest -lf tet_demo.py
12、-ff: 是–failed-first的缩写,会先运行失败的用例在执行其他用例(可能会重新执行,会导致重复运行fixture、setup/teardown)
pytest -ff test_demo.py
13、–setup-show:可以查看fixture的向后顺序
pytest --setup-show
三、fixture 基本用法
学过unittest的都知道里面用前置和后置setup和teardown非常好用,在每次用例开始前和结束后都去执行一次。
当然还有更高级一点的setupClass和teardownClass,需配合@classmethod装饰器一起使用,在做selenium自动化的时候,它的效率尤为突出,可以只启动一次浏览器执行多个用例。
pytest框架也有类似于setup和teardown的语法,并且还不止这四个
1)setup&teardown用例运行级别
-
模块级(setup_module/teardown_module)开始于模块始末,全局的
-
函数级(setup_function/teardown_function)只对函数用例生效(不在类中)
-
类级(setup_class/teardown_class)只在类中前后运行一次(在类中)
-
方法级(setup_method/teardown_method)开始于方法始末(在类中)
-
类里面的(setup/teardown)运行在调用方法的前后
#test_fixtclass.py # coding:utf-8 import pytest # 类和方法 class TestCase(): def setup(self): print("setup: 每个用例开始前执行") def teardown(self): print("teardown: 每个用例结束后执行") def setup_class(self): print("setup_class:所有用例执行之前") def teardown_class(self): print("teardown_class:所有用例执行之前") def setup_method(self): print("setup_method: 每个用例开始前执行") def teardown_method(self): print("teardown_method: 每个用例结束后执行") def test_one(self): print("正在执行----test_one") x = "this" assert 'h' in x def test_two(self): print("正在执行----test_two") x = "hello" assert hasattr(x, 'check') def test_three(self): print("正在执行----test_three") a = "hello" b = "hello world" assert a in b if __name__ == "__main__": pytest.main(["-s", "test_fixtclass.py"])
运行结果
test_fixtclass.py setup_class:所有用例执行之前 setup_method: 每个用例开始前执行 setup: 每个用例开始前执行 正在执行----test_one .teardown: 每个用例结束后执行 teardown_method: 每个用例结束后执行 setup_method: 每个用例开始前执行 setup: 每个用例开始前执行 正在执行----test_two Fteardown: 每个用例结束后执行 teardown_method: 每个用例结束后执行 setup_method: 每个用例开始前执行 setup: 每个用例开始前执行 正在执行----test_three .teardown: 每个用例结束后执行 teardown_method: 每个用例结束后执行 teardown_class:所有用例执行之前
运行的优先级:setup_class》setup_method》setup 》用例》teardown》teardown_method》teardown_class
备注:这里setup_method和teardown_method的功能和setup/teardown功能是一样的,一般二者用其中一个即可
2)fixture使用
firture相对于setup和teardown来说应该有以下几点优势
- 命名方式灵活,不局限于setup和teardown这几个命名;
- conftest.py 配置里可以实现数据共享,不需要import就能自动找到一些配置;
- scope=“module” 可以实现多个.py跨文件共享前置, 每一个.py文件调用一次;
- scope=“session” 以实现多个.py跨文件使用一个session来完成多个用例;
- firture 的灵活还体现在,fixture 可以同时使用多个,且 fixture 间可以相互调用;
fixture(scope="function", params=None, autouse=False, ids=None, name=None):
"""使用装饰器标记fixture的功能
可以使用此装饰器(带或不带参数)来定义fixture功能。 fixture功能的名称可以在以后使用
引用它会在运行测试之前调用它:test模块或类可以使用pytest.mark.usefixtures(fixturename标记,
测试功能可以直接使用fixture名称作为输入参数,在这种情况下,夹具实例从fixture返回功能将被注入。
:arg scope: scope 有四个级别参数 "function" (默认), "class", "module" or "session".
:arg params: 一个可选的参数列表,它将导致多个参数调用fixture功能和所有测试使用它
:arg autouse: 如果为True,则为所有测试激活fixture func 可以看到它。 如果为False(默认值)则显式需要参考来激活fixture
:arg ids: 每个字符串id的列表,每个字符串对应于params 这样他们就是测试ID的一部分。 如果没有提供ID它们将从params自动生成
:arg name: fixture的名称。 这默认为装饰函数的名称。 如果fixture在定义它的同一模块中使用,夹具的功能名称将被请求夹具的功能arg遮蔽; 解决这个问题的一种方法是将装饰函数命名“fixture_ <fixturename>”然后使用”@ pytest.fixture(name ='<fixturename>')"""。
示例
# 新建一个文件test_demo.py
# coding:utf-8
import pytest
# 不带参数时默认scope="function"
@pytest.fixture()
def login():
print("输入账号,密码先登录")
def test_1(login):
print("用例1:登录之后执行1")
def test_2(): # 不传login
print("用例2:不需要登录执行2")
def test_3(login):
print("用例3:登录之后执行3")
if __name__ == "__main__":
pytest.main(["-s", "test_demo.py"])
运行结果
test_demo.py 输入账号,密码先登录
用例1:登录之后执行1
.用例2:不需要登录执行2
.输入账号,密码先登录
用例3:登录之后执3
.
3)conftest.py配置
上面一个例子是在同一个.py文件中,多个用例调用一个登陆功能,如果有多个.py的文件都需要调用登陆功能的话,那就不能把登陆写到用例里。而pytest有一个配置文件conftest.py,可以管理这些预置的操作场景,pytest会默认读取conftest.py里面的配置。
conftest.py配置需要注意以下点:
- conftest.py配置脚本名称是固定的,不能改名称
- conftest.py与运行的用例要在同一个pakage下,并且有____init____.py文件
- 不需要import导入 conftest.py,pytest用例会自动查找
__init__.py
conftest.py
# coding:utf-8
import pytest
@pytest.fixture()
def login():
print("输入账号,密码先登录")
test_demo_1.py
# coding:utf-8
import pytest
def test_1(login):
print("用例1:登录之后执行1")
def test_2(): # 不传login
print("用例2:不需要登录执行2")
def test_3(login):
print("用例3:登录之后执行3")
if __name__ == "__main__":
pytest.main(["-s", "test_demo_1.py"])
test_demo_2.py
# coding:utf-8
import pytest
def test_4(login):
print("用例4:登录之后执行1")
def test_5(): # 不传login
print("用例5:登录之后执行3")
if __name__ == "__main__":
pytest.main(["-s", "test_demo_2.py"])
4)fixture 的scope控制setup作用域
scope 有四个级别参数 “function” (默认), “class”, “module” or "session"的区别
- function :默认的作用域,如果被调用每个测试方法都会执行一次,默认时可以不用指定;(注意,当function级别的fixture修饰测试类上时,测试类下的每个方法都会调用执行一次这个 fixture)
- class :每个测试类执行一次,如果同个测试类中多次调用同一个 fixture 也只会执行一次;
- module :每个测试模块执行一次,如果同个测试模块多次调用同一个 fixture 也只会执行一次;
- package :每个 package 中只执行一次,如果同个 package 下多次调用,也只会执行一次(可能淘汰,不建议使用);
- session :每次测试会话中只执行一次,在同一会话中多次调用,也只会执行一次;
代码示例
# conftest.py
import pytest
# 作用域 function
@pytest.fixture(scope='function')
def fix_func():
print('\n方法级:fix_func...')
# 作用域 class
@pytest.fixture(scope='class')
def fix_class():
print('\n类级:fix_class...')
# 作用域 module
@pytest.fixture(scope='module')
def fix_module():
print('\n模块级:fix_module...')
# 作用域 session
@pytest.fixture(scope='session')
def fix_session():
print('\n会话级:fix_session...')
# test_demo_1.py
import pytest
class TestClass_1:
"""测试类1"""
@pytest.mark.usefixtures('fix_func')
@pytest.mark.usefixtures('fix_class')
@pytest.mark.usefixtures('fix_module')
@pytest.mark.usefixtures('fix_session')
def test_1(self):
print("测试用例1")
@pytest.mark.usefixtures('fix_func')
@pytest.mark.usefixtures('fix_class')
@pytest.mark.usefixtures('fix_module')
@pytest.mark.usefixtures('fix_session')
def test_2(self):
print("测试用例2")
class TestClass_2:
"""测试类2"""
@pytest.mark.usefixtures('fix_func')
@pytest.mark.usefixtures('fix_class')
@pytest.mark.usefixtures('fix_module')
@pytest.mark.usefixtures('fix_session')
def test3(self):
print("测试用例3")
if __name__ == '__main__':
pytest.main(['-s'])
运行结果
collected 3 items
test_demo_1.py
会话级:fix_session...
模块级:fix_module...
类级:fix_class...
方法级:fix_func...
测试用例1
.
方法级:fix_func...
测试用例2
.
类级:fix_class...
方法级:fix_func...
测试用例3
.
============================== 3 passed in 0.02s ===============================
在测试类使用function级别的fixture示例
# test_demo.py
import pytest
@pytest.mark.usefixtures('fix_func') # conftest.py 还是使用上个示例的
class TestCase:
def test_1(self):
print("测试用例1")
def test_2(self):
print("测试用例2")
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
collected 2 items
test_demo.py
方法级:fix_func...
测试用例1
.
方法级:fix_func...
测试用例2
.
============================== 2 passed in 0.01s ===============================
5)fixture 中yield的使用
1.Fixtures可以使用yield 或者 return 语句为测试函数提供它们的值,当使用yield语句,在它之后的代码块作为拆卸代码执行,而且不管测试结果如何,fixture功能必须只产生一次。
2.通过上面示例可以看到fixture通过scope参数控制setup级别作为用例之前的操作,用例执行完之后肯定也会有teardown操作。而pytest是用yield关键字呼唤teardown操作。
示例代码:
# conftest.py
import pytest
@pytest.fixture(scope='function')
def fix_fun2():
print("\n测试用例执行前,执行fix_fun2前置操作")
n = 1
yield n
print("测试用例执行前,执行fix_fun2后置操作")
# test_demo_3.py
import pytest
@pytest.mark.usefixtures("fix_fun2")
class TestDemo2:
def test_fun4(self, fix_fun2):
print("测试用例4")
print(f'yield 返回值:{fix_fun2}')
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
test_demo_3.py
测试用例执行前,执行fix_fun2前置操作
测试用例4
yield 返回值:1
.测试用例执行前,执行fix_fun2后置操作
============================== 1 passed in 0.01s ===============================
6)fixture 的相互调用、使用多个
前面讲过,fixture 可以同时使用多个,且 fixture 间可以相互调用
在上个代码示例基础上我们改一下:
# conftest.py
import pytest
@pytest.fixture(scope='function')
def fix_fun2():
print("\n测试用例执行前,执行fix_fun2前置操作")
yield "<fix_fun2 yield2>"
print("测试用例执行前,执行fix_fun2后置操作")
@pytest.fixture()
def fix_fun3(fix_fun2): # 调用 fix_fun2
print("\n测试用例执行前,执行fix_fun3前置操作")
yield fix_fun2 # 把fix_fun2 作为返回值
print("测试用例执行前,执行fix_fun3后置操作")
@pytest.fixture()
def fix_fun4():
print("\n测试用例执行前,执行fix_fun4前置操作")
yield "<fix_fun2 yield4>"
print("测试用例执行前,执行fix_fun4后置操作")
# test_demo_3.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time : 2020/11/21 12:43 下午
# @Author : Yio
import pytest
@pytest.mark.usefixtures("fix_fun3")
@pytest.mark.usefixtures("fix_fun4")
class TestDemo2:
def test_fun4(self, fix_fun3, fix_fun4):
print("测试用例4")
print(f'yield 返回值:{fix_fun3}, {fix_fun4}')
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
collected 1 item
test_demo_3.py
测试用例执行前,执行fix_fun4前置操作
测试用例执行前,执行fix_fun2前置操作
测试用例执行前,执行fix_fun3前置操作
测试用例4
yield 返回值:<fix_fun2 yield2>, <fix_fun2 yield4>
.测试用例执行前,执行fix_fun3后置操作
测试用例执行前,执行fix_fun2后置操作
测试用例执行前,执行fix_fun4后置操作
============================== 1 passed in 0.02s ===============================
7)fixture的执行顺序
从上个示例的运行结果来看,fixture的执行顺序是有它的一套规则的。
当一个测试方法同时调用多个fixture时,会根据各fixture作用域、依赖关系、传参顺序来确定fixture的执行顺序。
执行顺序要分两种情况:
a. 用例前置(yield 之前):
- 先按作用域,先执行作用域高的(先session级,最后执行function级);
- 再按依赖关系,在执行某个fixture时,如果它调用了其他的fixture那就先执行被调用的fixture;
- 再按传参顺序,在测试方法中如果f1的传参位置在f2的前面,那么就先执行f1,再执行f2;
- 如果是用装饰器的方法使用fixtures,那么下方的fixture先执行,上方的fixture后执行;
- 如果同时用了传参和装饰器的方法使用fixtures,那么先执行装饰器的fixture,再执行传参的fixture;
b**. 用例后置(yield 之后):**
- 完全和前置的顺序相反,同一个 fixture 的前置越晚执行,后置就越早执行!
我们通过示例来看:
# conftest.py
import pytest
@pytest.fixture
def fix_func():
print('\n方法级-被调用方...')
yield
print('\n方法级-被调用方...')
@pytest.fixture
def fix_func_param_1(fix_func):
print('\n方法级-传参法-1...')
yield
print('\n方法级-传参法-1...')
@pytest.fixture
def fix_func_param_2():
print('\n方法级-传参法-2...')
yield
print('\n方法级-传参法-2...')
@pytest.fixture
def fix_func_param_3():
print('\n方法级-传参法-3...')
yield
print('\n方法级-传参法-3...')
@pytest.fixture
def fix_func_decorator_1(fix_func):
print('\n方法级-装饰器-1...')
yield
print('\n方法级-装饰器-1...')
@pytest.fixture
def fix_func_decorator_2():
print('\n方法级-装饰器-2...')
yield
print('\n方法级-装饰器-2...')
@pytest.fixture
def fix_func_decorator_3():
print('\n方法级-装饰器-3...')
yield
print('\n方法级-装饰器-3...')
# 作用域 class
@pytest.fixture(scope='class')
def fix_class():
print('\n类级:fix_class...')
yield
print('\n类级:fix_class...')
# 作用域 module
@pytest.fixture(scope='module')
def fix_module():
print('\n模块级:fix_module...')
yield
print('\n模块级:fix_module...')
# 作用域 session
@pytest.fixture(scope='session')
def fix_session():
print('\n会话级:fix_session...')
yield
print('\n会话级:fix_session...')
# test_demo_3.py
import pytest
class TestClass:
"""测试类1"""
@pytest.mark.usefixtures('fix_class')
@pytest.mark.usefixtures('fix_session')
@pytest.mark.usefixtures('fix_func_decorator_3')
@pytest.mark.usefixtures('fix_func_decorator_2')
@pytest.mark.usefixtures('fix_func_decorator_1')
def test_func(self, fix_func_param_1, fix_func_param_2, fix_func_param_3, fix_module):
print('==============>>>执行测试用例<<<==============')
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
collected 1 item
test_demo_3.py
会话级:fix_session...
模块级:fix_module...
类级:fix_class...
方法级-被调用方...
方法级-装饰器-1...
方法级-装饰器-2...
方法级-装饰器-3...
方法级-传参法-1...
方法级-传参法-2...
方法级-传参法-3...
==============>>>执行测试用例<<<==============
.
方法级-传参法-3...
方法级-传参法-2...
方法级-传参法-1...
方法级-装饰器-3...
方法级-装饰器-2...
方法级-装饰器-1...
方法级-被调用方...
类级:fix_class...
模块级:fix_module...
============================== 1 passed in 0.02s ===============================
c.fixture 的 autouse 参数:
autouse 参数的作用是 自动执行,它会在相应的作用域中不需要调度,自动执行,并且在同一作用域中优先级最高;
# conftest.py
import pytest
# 增加一个 fix_auto 夹具
@pytest.fixture(autouse=True)
def fix_auto():
print('\n方法级-自动执行...')
yield
print('\n方法级-自动执行...')
# test_demo_3.py
import pytest
class TestClass:
"""测试类1"""
@pytest.mark.usefixtures('fix_class')
@pytest.mark.usefixtures('fix_session')
@pytest.mark.usefixtures('fix_func_decorator_1')
def test_func(self, fix_func_param_1, fix_module):
print('==============>>>执行测试用例<<<==============')
运行结果
============================= test session starts =============================
test_demo_3.py::TestClass::test_func
会话级:fix_session...
模块级:fix_module...
类级:fix_class...
方法级-自动执行...
方法级-被调用方...
方法级-装饰器-1...
方法级-传参法-1...
==============>>>执行测试用例<<<==============
PASSED
方法级-传参法-1...
方法级-装饰器-1...
方法级-被调用方...
方法级-自动执行...
类级:fix_class...
模块级:fix_module...
会话级:fix_session...
============================== 1 passed in 0.03s ==============================
8)fixture 的默认参数request及indirect=True参数
@pytest.fixture装饰器,传参就用默认的request参数, request.param 这是接收传入的参数,添加indirect=True参数是为了把fixture当成一个函数去执行,而不是一个参数。
示例:
一个参数的用法
# test_demo.py
import pytest
# 测试账号数据
test_user_data = ["admin1", "admin2"]
@pytest.fixture(scope="module")
def login(request):
user = request.param
print("登录账户:%s"%user)
return user
@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"])
多个参数的用法
# test_demo.py
import pytest
test_user_data = [{"user": "Tom", "pwd": "111111"},
{"user": "Vicky", "pwd": ""}]
@pytest.fixture(scope="module")
def login(request):
user = request.param["user"]
pwd = request.param["pwd"]
print(f"登录账户:{user}")
print(f"登录密码:{pwd}")
if pwd:
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(['-rs'])
9)设置autouse=True
fixture里面有个参数autouse,默认是Fasle没开启的,设置为True开启自动使用该fixture功能,这样用例就不用每次都去传参,会自动按照fixture的执行顺序去执行。
示例:
import pytest
@pytest.fixture(scope='function', autouse=True)
def open_browse(request):
print('function:打开浏览器!')
yield
print('function:关闭浏览器!')
@pytest.fixture(scope='class', autouse=True)
def open_home_page():
print('class:打开主页!')
class TestO1:
def test_o1(self):
print('开始执行用例1')
def test_02(self):
print('开始执行用例2')
class Test02:
def test_o3(self):
print('开始执行用例3')
def test_04(self):
print('开始执行用例4')
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
collected 4 items
test_demo.py class:打开主页!
function:打开浏览器!
开始执行用例1
.function:关闭浏览器!
function:打开浏览器!
开始执行用例2
.function:关闭浏览器!
class:打开主页!
function:打开浏览器!
开始执行用例3
.function:关闭浏览器!
function:打开浏览器!
开始执行用例4
.function:关闭浏览器!
============================== 4 passed in 0.01s ===============================
10) cache使用
pytest 运行完用例之后会生成 .pytest_cache 缓存文件夹,用于记录用例的ids和上一次失败的用例。方便我们使用cache
参数说明:
- –lf 也可以使用
--last-failed
仅运行上一次失败的用例 - –ff 也可以使用
--failed-first
运行全部的用例,但是上一次失败的用例先运行 - –nf 也可以使用
--new-first
根据文件插件的时间,新的测试用例会先运行 - –cache-show=[CACHESHOW] 显示.pytest_cache文件内容,不会收集用例也不会测试用例,选项参数: glob (默认: ‘*’)
- –cache-clear 测试之前先清空.pytest_cache文件
Pytest运行完成后,会在当前的目录生成一个 .pytest_cache
的缓存文件夹,层级结构如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rmeldc1G-1608128106379)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201214205804243.png)]
这是我上一次执行的结果:
=========================== short test summary info ============================
FAILED test_demo_1.py::TestO1::test_01 - assert False
========================= 1 failed, 2 passed in 1.07s ==========================
11) fixture 的name参数
上面fixture的参数介绍的时候,有提到name的参数说明,实际上它的作用是给fixture设置一个别名
name: fixture的名称。 这默认为装饰函数的名称。 如果fixture在定义它的同一模块中使用,夹具的功能名称将被请求夹具的功能arg遮蔽; 解决这个问题的一种方法是将装饰函数命名“fixture_ <fixturename>”然后使用”@ pytest.fixture(name ='<fixturename>')"""
现在我们来演示一下它的作用
# test_demo.py
import pytest
class TestO1:
@pytest.fixture(name='driver')
def login(self):
print('执行前')
yield
print('执行后')
def test_01(self, driver):
"""测试用例1执行了"""
assert 2 < 1
if __name__ == '__main__':
pytest.main(["-s"])
运行结果:
ollecting ... collected 1 item
test_demo_1.py::TestO1::test_01 执行前
FAILED [100%]
test_demo_1.py:14 (TestO1.test_01)
self = <TestCase.test_demo_1.TestO1 object at 0x10a03b390>, driver = None
def test_01(self, driver):
"""测试用例1执行了"""
> assert 2 < 1
E assert 2 < 1
test_demo_1.py:17: AssertionError
执行后
断言失败
=================================== FAILURES ===================================
________________________________ TestO1.test_01 ________________________________
self = <TestCase.test_demo_1.TestO1 object at 0x10a03b390>, driver = None
def test_01(self, driver):
"""测试用例1执行了"""
> assert 2 < 1
E assert 2 < 1
test_demo_1.py:17: AssertionError
---------------------------- Captured stdout setup -----------------------------
执行前
--------------------------- Captured stdout teardown ---------------------------
执行后
=========================== short test summary info ============================
FAILED test_demo_1.py::TestO1::test_01 - assert 2 < 1
============================== 1 failed in 0.06s ===============================
进程已结束,退出代码1
四、Pytest 插件
1)、Pytest-html 插件
pytest的优秀之处前面已经提过,它有非常多三方插件支持和自定义开发插件。其中一个插件pytest-HTML,是用于生成测试结果的HTML报告。
a. github上源码地址
【https://github.com/pytest-dev/pytest-html】
b. 安装
pip3 install pytest-html
c. 执行方法
pytest --html=report.html
# 直接执行"pytest --html=report.html"生成的报告会在当前脚本的同一路径,如果想指定报告的存放位置,放到当前脚本的同一目录下的report文件夹里
pytest --html=./report/report.html
我这里就不再具体去描述这个插件,因为我们有更好、更强大的选择-Allure,后面会说到。
2) 分布式执行 pytest-xdist 插件
a. 插件介绍
pytest-xdist插件扩展了一些独特的测试执行模式pytest:
- 测试运行并行化:如果有多个CPU或主机,则可以将它们用于组合测试运行。会加快运行速度
- –looponfail:在子进程中重复运行测试。每次运行之后,pytest会等待,直到项目中的文件发生更改,然后重新运行以前失败的测试。
重复此过程直到所有测试通过,之后再次执行完整运行。 - 多平台覆盖:您可以指定不同的Python解释器或不同的平台,并在所有平台上并行运行测试。
在远程运行测试之前,pytest有效地将您的程序源代码“rsyncs”到远程位置。报告所有测试结果并显示给您的本地终端。您可以指定不同的Python版本和解释器。
GitHub地址:https://github.com/pytest-dev/pytest-xdist
分布式实现的原理:https://github.com/pytest-dev/pytest-xdist/blob/master/OVERVIEW.md
b. 安装及查看版本:
pip install pytest-xdist # 安装
pip show pytest-xdist # 查看版本
c. 并行测试
多cpu并行执行用例,直接加个-n参数即可,后面num参数就是并行数量,比如num设置为2
pytest -n 2
pytest.main(['-s', '-n 2']) # pytest.main 函数用法
3) 重复执行pytest-repeat 插件
- 自动化运行用例时候,也会出现偶然的bug,可以针对单个用例,或者针对某个模块的用例重复执行多次;
- 在回归测试中,针对不稳定的功能,进行重复测试直到复习问题的场景,pytest-repeat 插件提供了这个帮助;
a. 安装
pip3 install pytest-repeat
b. 启动命令方式重跑
–count=2 或者 --count 2
pytest --count=2
c. 重复测试直到失败(重要)
-
如果需要验证偶现问题,可以一次又一次地运行相同的测试直到失败,这个插件将很有用
-
可以将pytest的 -x 选项与pytest-repeat结合使用,以强制测试运行程序在第一次失败时停止
-
pytest -x --count 1000
d.装饰器的方式
如果要在代码中将某些测试用例标记为执行重复多次,可以使用 @pytest.mark.repeat(count)
@pytest.mark.repeat(2)
def test_01(self):
print('开始执行用例1')
e.–repeat-scope
命令行参数
**作用:**可以覆盖默认的测试用例执行顺序,类似fixture的scope参数
- function:默认,范围针对每个用例重复执行,再执行下一个用例
- class:以class为用例集合单位,重复执行class里面的用例,再执行下一个
- module:以模块为单位,重复执行模块里面的用例,再执行下一个
- session:重复整个测试会话,即所有测试用例的执行一次,然后再执行第二次
示例:
# test_demo.py
import pytest
class TestO1:
def test_o1(self):
print('开始执行用例1')
def test_02(self):
print('开始执行用例2')
class Test02:
def test_o3(self):
print('开始执行用例3')
def test_04(self):
print('开始执行用例4')
if __name__ == '__main__':
pytest.main(['-v', '--count=2', '-repeat-scope=class'])
运行结果:
plugins: xdist-2.1.0, repeat-0.9.1, forked-1.3.0
collecting ... collected 8 items
test_demo.py::TestO1::test_o1[1-2] PASSED [ 12%]
test_demo.py::TestO1::test_o1[2-2] PASSED [ 25%]
test_demo.py::TestO1::test_02[1-2] PASSED [ 37%]
test_demo.py::TestO1::test_02[2-2] PASSED [ 50%]
test_demo.py::Test02::test_o3[1-2] PASSED [ 62%]
test_demo.py::Test02::test_o3[2-2] PASSED [ 75%]
test_demo.py::Test02::test_04[1-2] PASSED [ 87%]
test_demo.py::Test02::test_04[2-2] PASSED [100%]
============================== 8 passed in 0.05s ===============================
f.兼容性问题
pytest-repeat不能与unittest.TestCase测试类一起使用。无论–count设置多少,这些测试始终仅运行一次,并显示警告
4) 时查看用例报错内容 pytest-instafail
当测试执行遇到失败或错误时能及时看到详细的堆栈信息
a.安装
pip install pytest-instafail
示例:
# test_demo.py
import pytest
from time import sleep
class TestO1:
@pytest.fixture(autouse=True)
def login(self):
print('执行前')
yield
print('执行后')
@pytest.mark.usefixtures('login')
def test_01(self, login):
"""测试用例1执行了"""
assert 2 < 1
def test_02(self):
pass
def test_03(self):
sleep(1)
if __name__ == '__main__':
pytest.main(["--tb=line", "--instafail"]) # 和--tb=line效果更好
运行结果:
collecting ... collected 3 items
test_demo_1.py::TestO1::test_01 执行前
FAILED [ 33%]
test_demo_1.py:14 (TestO1.test_01)
self = <TestCase.test_demo_1.TestO1 object at 0x10ed34a10>, login = None
@pytest.mark.usefixtures('login')
def test_01(self, login):
"""测试用例1执行了"""
> assert 2 < 1
E assert 2 < 1
test_demo_1.py:18: AssertionError
执行后
执行前
PASSED [ 66%]执行后
执行前
PASSED [100%]执行后
断言失败
test_demo_1.py::TestO1::test_02
test_demo_1.py::TestO1::test_03
=================================== FAILURES ===================================
________________________________ TestO1.test_01 ________________________________
self = <TestCase.test_demo_1.TestO1 object at 0x10ed34a10>, login = None
@pytest.mark.usefixtures('login')
def test_01(self, login):
"""测试用例1执行了"""
> assert 2 < 1
E assert 2 < 1
test_demo_1.py:18: AssertionError
---------------------------- Captured stdout setup -----------------------------
执行前
--------------------------- Captured stdout teardown ---------------------------
执行后
=========================== short test summary info ============================
FAILED test_demo_1.py::TestO1::test_01 - assert 2 < 1
========================= 1 failed, 2 passed in 1.08s ==========================
进程已结束,退出代码1
五、Pytest参数化parametrize
unittest有ddt,pytest当然也有他们的参数化parametrize
1)一般用法
# 一个参数的时候
@pytest.mark.parametrize(“参数名”,列表数据)
# 多个参数的时候
@pytest.mark.parametrize(“参数名1,参数名2”,列表嵌套元组数据/列表嵌套字典)
示例
# test_demo.py
import pytest
@pytest.mark.parametrize("test_input,expected",
[("3+5", 8),
("2+4", 6),
("6 * 9", 42),
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
if __name__ == '__main__':
pytest.main(['-s'])
运行结果
collected 3 items
test_demo.py ..F
=================================== FAILURES ===================================
_____________________________ test_eval[6 * 9-42] ______________________________
test_input = '6 * 9', expected = 42
@pytest.mark.parametrize("test_input,expected",
[("3+5", 8),
("2+4", 6),
("6 * 9", 42),
])
def test_eval(test_input, expected):
> assert eval(test_input) == expected
E AssertionError: assert 54 == 42
E + where 54 = eval('6 * 9')
test_demo.py:12: AssertionError
=========================== short test summary info ============================
FAILED test_demo.py::test_eval[6 * 9-42] - AssertionError: assert 54 == 42
========================= 1 failed, 2 passed in 0.05s ==========================
2) 在参数化中使用标记
标记单个测试实例在参数化,例如使用内置的mark.xfail
# test_demo.py
import pytest
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
pytest.param("6 * 9", 42, marks=pytest.mark.xfail),
])
def test_eval(test_input, expected):
print("-------开始用例------")
assert eval(test_input) == expected
if __name__ == '__main__':
pytest.main(['-s'])
运行结果
collected 3 items
test_demo.py -------开始用例------
.-------开始用例------
.-------开始用例------
x
========================= 2 passed, 1 xfailed in 0.05s =========================
3) 组合参数
1.若要获得多个参数化参数的所有组合,可以堆叠参数化装饰器
# test_demo.py
import pytest
@pytest.mark.parametrize('x', [1, 3])
@pytest.mark.parametrize('y', [1, 2, 3])
def test_demo(x, y):
print(f'\n x:{x} y:{y}')
if __name__ == '__main__':
pytest.main(['-s'])
运行结果
collected 6 items
test_demo.py
x:1 y:1
.
x:3 y:1
.
x:1 y:2
.
x:3 y:2
.
x:1 y:3
.
x:3 y:3
.
============================== 6 passed in 0.01s ===============================
4) ids参数
ids用来传递测试用例的序列,可在控制台打印出来,也可以结果allure在测试套件中显示
示例
# test_demo.py
import pytest
test_data = [{'a': 1, 'b': 2}, {'a': 2, 'b': 3}] # 列表嵌套字典的方式,在做接口自动化的时候比较常见
test_case_id = ['case 001', 'case 002']
@pytest.mark.parametrize('x', test_data, ids=test_case_id)
def test_demo(x):
assert x['a'] * x['b'] > 1
if __name__ == '__main__':
pytest.main(['-v'])
运行结果:
collecting ... collected 2 items
test_demo.py::test_demo[case 001] PASSED [ 50%]
test_demo.py::test_demo[case 002] PASSED [100%]
============================== 2 passed in 0.01s ===============================
六、断言的使用
pytest里面断言实际上就是python里面的assert断言方法,常用的有以下几种
- assert xx 判断xx为真
- assert not xx 判断xx不为真
- assert a in b 判断b包含a
- assert a == b 判断a等于b
- assert a != b 判断a不等于b
这是python的基础语法,就不详细说了
import pytest
def test_demo():
a = 'test'
b = 't'
assert b not in a, f"判断b:{b} in a:{a}" # 这里可以加入说明,在断言失败的时候,会打印
if __name__ == '__main__':
pytest.main(['-v'])
运行结果:
collected 1 item
test_demo.py F
=================================== FAILURES ===================================
__________________________________ test_demo ___________________________________
def test_demo():
a = 'test'
b = 't'
> assert b not in a, f"判断b:{b} in a:{a}"
E AssertionError: 判断b:t in a:test
E assert 't' not in 'test'
E 't' is contained here:
E test
E ? +
test_demo.py:9: AssertionError
=========================== short test summary info ============================
FAILED test_demo.py::test_demo - AssertionError: 判断b:t in a:test
============================== 1 failed in 0.05s ===============================
七、skip跳过用例
1)pytest.mark.skip
可以标记无法在某些平台上运行的测试功能,或者您希望失败的测试功能
skip意味着只有在满足某些条件时才希望测试通过,否则pytest应该跳过运行测试。 常见示例是在非Windows平台上跳过仅限Windows的测试,或跳过测试依赖于当前不可用的外部资源(例如数据库)。
xfail意味着您希望测试由于某种原因而失败。 一个常见的例子是对功能的测试尚未实施,或尚未修复的错误。 当测试通过时尽管预计会失败(标有pytest.mark.xfail),它是一个xpass,将在测试摘要中报告。
pytest计数并分别列出skip和xfail测试。 未显示有关跳过/ xfailed测试的详细信息默认情况下,以避免混乱输出。 您可以使用-r选项查看与“short”字母对应的详细信息显示在测试进度中
pytest -rxXs # show extra info on xfailed, xpassed, and skipped tests
有关-r选项的更多详细信息,请运行pytest -h
示例:
# test_demo.py
import pytest
@pytest.mark.skip(reason='跳过次用例,不执行!')
def test_demo():
pass
def test_demo_1():
pass
if __name__ == '__main__':
pytest.main(['-rs'])
运行结果:
collected 2 items
test_demo.py s. [100%]
=========================== short test summary info ============================
SKIPPED [1] test_demo.py:6: 跳过次用例,不执行!
========================= 1 passed, 1 skipped in 0.01s =========================
2)pytest.mark.skipif
如果你希望有条件的跳过测试用例,你可以改用skipif
示例:
import pytest
import sys
# 你希望测试用例在python 3.6以下的宝宝运行
@pytest.mark.skipif(sys.version_info > (3, 6), reason="请在python 3.6 以下的版本运行")
def test_function():
pass
if __name__ == '__main__':
pytest.main(['-rs'])
运行结果:
============================= test session starts ==============================
platform darwin -- Python 3.7.7, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/***/PycharmProjects/Demo
collected 1 item
test_demo.py s [100%]
=========================== short test summary info ============================
SKIPPED [1] test_demo.py:8: 请在python 3.6 以下的版本运行
============================== 1 skipped in 0.01s ==============================
您可以在模块之间共享skipif标记。参考以下官方文档案例
# content of test_mymodule.py
import mymodule
minversion = pytest.mark.skipif(mymodule.__versioninfo__ < (1,1),
reason="at least mymodule-1.1 required")
@minversion
def test_function():
...
# test_myothermodule.py
from test_mymodule import minversion
@minversion
def test_anotherfunction():
警告:强烈建议不要在使用继承的类上使用skipif。 pytest中的一个已知错误标记可能会导致超类中的意外行为。
3)通过标记xfail 跳过
pytest里面用xfail标记用例为失败的用例,会直接跳过后续代码的执行,所以当用例a失败的时候,如果用例b依赖于用例a的结果,那可以直接跳过用例b的后续测试,直接给他标记失败xfail。
示例
# test_demo.py
import pytest
test_data = ["Tom", ""]
@pytest.fixture(scope="module")
def login(request):
user = request.param
print(f"登录账户:{user}")
if user:
return True
else:
return False
# indirect=True 声明login是个函数
@pytest.mark.parametrize("login", test_data, indirect=True)
def test_login(login):
a = login
if not a:
pytest.xfail('用户名不存在,登录失败')
print("测试用例中login的返回值:%s" % a)
assert a, "失败原因:用户名为空"
if __name__ == '__main__':
pytest.main(['-s'])
运行结果:
collected 2 items
test_demo.py 登录账户:Tom
测试用例中login的返回值:True
.登录账户:
x
========================= 1 passed, 1 xfailed in 0.05s =========================
八、配置文件pytet.ini
pytest配置文件可以改变pytest的运行方式,它是一个固定的文件pytest.ini文件,读取配置信息,按指定的方式去运行。
pytest里面有些文件是非test文件
- pytest.ini pytest的主配置文件,可以改变pytest的默认行为
- conftest.py 用于测试用例的一些fixture配置
- setup.cfg 也是ini格式文件,影响setup.py的行为
常用的配置有下面的
- –rsxX 表示pytest报告所有测试用例被跳过、预计失败、预计失败但实际被通过的原因
2.设置xfail_strict = true可以让那些标记为@pytest.mark.xfail但实际通过的测试用例被报告为失败
3.markers 有时候mark标签多了,不容易记住,为了方便后续执行指令的时候能准确使用mark的标签,可以写入到pytest.ini文件
# 保存为pytest.ini文件内的基本格式
[pytest]
addopts = -rsxX
xfail_strict = true
markers =
smoke: 执行冒烟级别的用例
P1: 执行主功能级别的用例
pytet --markers
运行结果:
@pytest.mark.smoke: 执行冒烟级别的用例
@pytest.mark.P1: 执行主功能级别的用例
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/stable/warnings.html#pytest-mark-filterwarnings
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
九、颜值担当-Allure报告
先上个图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vtbuI37y-1608128106381)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201204191501617.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yXvVuD5g-1608128106382)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201204194835274.png)]
官方介绍:
- Allure Framework是一种灵活的轻量级多语言测试报告工具,不仅可以以简洁的Web报告形式非常简洁地显示已测试的内容,也允许参与开发过程的每个人从日常测试中提取最大程度的有用信息
- 从开发/质量保证的角度来看,Allure报告可以缩短常见缺陷的生命周期:可以将测试失败划分为bug和损坏的测试,还可以配置log,step,fixture,attachments,timings,历史记录以及与TMS的集成以及Bug跟踪系统,因此负责任的开发人员和测试人员将掌握所有信息
- 从管理人员的角度来看,Allure提供了一个清晰的“全局”,涵盖了已涵盖的功能,缺陷聚集的位置,执行时间表的外观以及许多其他方便的事情
- Allure的模块化和可扩展性确保您始终能够微调某些东西,以使Allure更适合您
个人见解:
容易部署、美观、领导喜欢、能和Jenkins结合使用
1)安装插件
pip3 install allure-pytest
2) allure生成报告
通过–alluredir=allure报告生成文件夹路径,在pytest.mian中加入
在运行的时候加入终端执行的代码,就会自动在浏览器打开allure的报告展示
其中参数:–clean-alluredir 每次生成allure报告的时候会清除上一次报告生成的数据;
os.system("allure generate -o Report/Allure/")
os.system("allure serve Report/Allure/")
示例:
# run.py
import os
import pytest
if __name__ == '__main__':
pytest.main(["-s", "-v", "--alluredir=Report/Allure/AllureResult"])
os.system("allure generate -o Report/Allure/AllureResult")
os.system("allure serve Report/Allure/AllureResult")
3)Allure报告结构
- Overview:总览
- Categories:类别,默认是分了failed和error,凡是执行结果是其中一个的都会被归到类里面,可以通过这里快捷查看哪些用例是failed和error的
- Suites:测试套件,就是所有用例的层级关系,可以根据package、module、类、方法来查找用例
- Graphs:测试结果图形化,包括用例执行结果的分布图,优先级,耗时等
- Timeline:可以看到测试用例精确的测试时序(执行顺序),包括执行时间
- Behaviors:行为驱动,根据epic、feature、story来分组测试用例(后面会讲到)
- Packages:这就是按照package、module来分组测试用例了
4)Environment
可以理解成环境变量参数,没有什么实际作用,只是为了让别人知道本次测试的运行环境参数而已,显示啥都是自己定的,默认是没有的
在生产allure报告目录下,创建environment.properties或者environment.xml文件,注意内容不能有中文
这里就environment.properties示例
Browser=Chrome
Browser.Version=81
Stand=Production
ApiUrl=127.0.0.1
python.Version=3.7
生成报告显示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yeXjvREu-1608128106384)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201208221127489.png)]
5)@allure.step 装饰器
- allure报告最重要的一点是,它允许对每个测试用例进行非常详细的步骤说明;
- 通过 @allure.step() 装饰器,可以在allure报告中显示更详细的测试过程;
- step() 只有一个参数title,根据传入的title在allure上显示
示例:
# test_demo.py
import pytest
import allure
import os
@pytest.fixture(scope='function', autouse=True)
def open_browse(request):
print('function:打开浏览器!')
yield
print('function:关闭浏览器!')
@pytest.fixture(scope='class', autouse=True)
def open_home_page():
print('class:打开主页!')
@allure.step('步骤1')
def run_step_1():
pass
@allure.step('步骤2')
def run_step_2():
pass
class TestO1:
def test_o1(self):
run_step_1()
run_step_2()
def test_02(self):
print('开始执行用例2')
class Test02:
def test_o3(self):
print('开始执行用例3')
def test_04(self):
print('开始执行用例4')
if __name__ == '__main__':
pytest.main(["-s", "-v", "--alluredir=Report/Allure/AllureResult"])
os.system("allure generate -o Report/Allure/AllureResult")
os.system("allure serve Report/Allure/AllureResult")
运行结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hiDfLgSC-1608128106385)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201208222915747.png)]
6)allure.attach 附件上传
**作用:**allure报告还支持显示许多不同类型的附件,可以补充测试结果;自己想输出啥就输出啥,挺好的
语法: allure.attach(body, name, attachment_type, extension)
这个用法是allure根据我们传入的body和文件名name生成一个附件上传到报告上
参数列表
-
body:要显示在allure上附件里展示的内容
-
name:附件名字
-
attachment_type:附件类型,是 allure.attachment_type 里面的其中一种
-
extension:附件的扩展名(比较少用)
attach还有另外一个用法:allure.attach.file
语法:allure.attach.file(source, name, attachment_type, extension)
而这个用法是把我们本地的附件上传到报告上
- source:要上传的附件路径
- name:附件名字
- attachment_type:附件类型,是 allure.attachment_type 里面的其中一种
- extension:附件的扩展名(比较少用)
allure.attachment_type提供的类型有:
TEXT = ("text/plain", "txt")
CSV = ("text/csv", "csv")
TSV = ("text/tab-separated-values", "tsv")
URI_LIST = ("text/uri-list", "uri")
HTML = ("text/html", "html")
XML = ("application/xml", "xml")
JSON = ("application/json", "json")
YAML = ("application/yaml", "yaml")
PCAP = ("application/vnd.tcpdump.pcap", "pacp")
PDF = ("application/pdf", "pdf")
PNG = ("image/png", "png")
JPG = ("image/jpg", "jpg")
SVG = ("image/svg-xml", "svg")
GIF = ("image/gif", "gif")
BMP = ("image/bmp", "bmp")
TIFF = ("image/tiff", "tiff")
MP4 = ("image/mp4", "mp4")
OGG = ("image/ogg", "ogg")
WEBM = ("image/webm", "webm")
示例:
# test_demo.py
import pytest
import allure
import os
@pytest.fixture(scope='function', autouse=True)
def open_browse(request):
print('function:打开浏览器!')
yield
print('function:关闭浏览器!')
@pytest.fixture(scope='class', autouse=True)
def open_home_page():
print('class:打开主页!')
@allure.step('步骤1')
def run_step_1():
allure.attach("这是一个测试txt附件", 'test_data_1.text', allure.attachment_type.TEXT)
allure.attach.file('./Files/test_data.txt', 'test_data.txt', attachment_type=allure.attachment_type.TEXT)
@allure.step('步骤2')
def run_step_2():
pass
class TestO1:
def test_o1(self):
run_step_1()
run_step_2()
def test_02(self):
print('开始执行用例2')
class Test02:
def test_o3(self):
print('开始执行用例3')
def test_04(self):
print('开始执行用例4')
if __name__ == '__main__':
pytest.main(["-s", "-v", "--alluredir=Report/Allure/AllureResult"])
os.system("allure generate -o Report/Allure/AllureResult")
os.system("allure serve Report/Allure/AllureResult")
报告效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bGjNsAXS-1608128106386)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201209115059978.png)]
7) allure中添加用例描述
语法有三种
- @allure.description(str)
- 在测试用例函数声明下方添加 “”" “”"
- @allure.description_html(str):相当于传一个HTML代码组成的字符串,和上面说到的allure.attach() 中传HTML类似
注意:前面两种的效果和作用是一致的,我更喜欢用第二种,比较方便
示例:
# test_demo.py
import pytest
import allure
import os
@pytest.fixture(scope='function', autouse=True)
def open_browse(request):
print('function:打开浏览器!')
yield
print('function:关闭浏览器!')
@pytest.fixture(scope='class', autouse=True)
def open_home_page():
print('class:打开主页!')
@allure.step('步骤1')
def run_step_1():
allure.attach("这是一个测试txt附件", 'test_data_1.text', allure.attachment_type.TEXT)
allure.attach.file('./Files/test_data.txt', 'test_data.txt', attachment_type=allure.attachment_type.TEXT)
@allure.step('步骤2')
def run_step_2():
pass
class TestO1:
@allure.description("测试用例1执行了")
def test_o1(self):
run_step_1()
def test_02(self):
"""测试用例2执行了"""
print('开始执行用例2')
class Test02:
@allure.description_html("""
<h1>测试用例3的html说明</h1>
<table style="width:100%">
<tr>
<th>Firstname</th>
<th>Lastname</th>
</tr>
<tr align="center">
<td>William</td>
<td>Smith</td>
</table>
""")
def test_o3(self):
pass
def test_04(self):
pass
if __name__ == '__main__':
pytest.main(["-v", "--alluredir=Report/Allure/AllureResult"])
os.system("allure generate -o Report/Allure/AllureResult")
os.system("allure serve Report/Allure/AllureResult")
运行效果:
用法1:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iyjJxacY-1608128106387)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201209120853667.png)]
用法2:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U34vs9D0-1608128106387)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201209120946426.png)]
用法3:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lhnrRkhn-1608128106388)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201209121136406.png)]
8)测试用例的title显示
@allure.title()
作用
- 设置测试用例的标题,让报告更具有可读性,我们可以写成中文,总比函数名称要好很多
- 支持占位符传递关键字参数,结合 @pytest.mark.parametrize 使用实现动态标题
示例:
# test_demo.py
import pytest
import allure
import os
@pytest.fixture(scope='function', autouse=True)
def open_browse(request):
print('function:打开浏览器!')
yield
print('function:关闭浏览器!')
@pytest.fixture(scope='class', autouse=True)
def open_home_page():
print('class:打开主页!')
@allure.step('步骤1')
def run_step_1():
allure.attach("这是一个测试txt附件", 'test_data_1.text', allure.attachment_type.TEXT)
allure.attach.file('./Files/test_data.txt', 'test_data.txt', attachment_type=allure.attachment_type.TEXT)
@allure.step('步骤2')
def run_step_2():
pass
class TestO1:
login_data = [{'title': '测试有密码的情况', 'user': 'Tom', 'pwd': '111'}, {'title': '测试无密码的情况', 'user': 'Vicky', 'pwd': ''}]
test_case = ['测试有密码的情况', '测试无密码的情况']
@allure.title('测试用例1')
def test_01(self):
"""测试用例2执行了"""
run_step_1()
@allure.title('测试用例2:{login_data}')
@pytest.mark.parametrize('login_data', login_data)
def test_02(self, login_data):
"""测试用例2执行了"""
print(f'用户名:{login_data["user"]}, 密码{login_data["pwd"]}')
run_step_1()
run_step_2()
if login_data['pwd']:
assert True
else:
assert False
if __name__ == '__main__':
pytest.main(["-v", "--alluredir=Report/Allure/AllureResult", "--clean-alluredir"])
os.system("allure generate -o Report/Allure/AllureResult")
os.system("allure serve Report/Allure/AllureResult")
效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4VWYuN0R-1608128106388)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201211223849324.png)]
9)关联外部系统的链接地址
主要是为了将allure报告和测试管理系统集成,可以更快速的跳转到公司内部地址
- @allure.link()
- @allure.issue()
- @allure.testcase()
先看源码:
def link(url, link_type=LinkType.LINK, name=None):
return safely(plugin_manager.hook.decorate_as_link(url=url, link_type=link_type, name=name))
def issue(url, name=None):
return link(url, link_type=LinkType.ISSUE, name=name)
def testcase(url, name=None):
return link(url, link_type=LinkType.TEST_CASE, name=name)
- 可以看出issue()和testcase()其实调用的也是link(),只是link_type不一样
- 必传参数 url:跳转的链接
- 可选参数 name:显示在allure报告的名字,如果不传就是显示完整的链接,可读性不高,建议必传;
- 可以理解成:三个方法是一样的,我们都提供跳转链接和名字,只是链接的type不一样,最终显示出来的样式不一样****而已【type不一样,样式不一样】
- 如果你喜欢,只用@allure.link()也可以
- 而出现三个装饰器的原因是为了更好地将链接分类【访问连接、Bug链接、测试用例链接】
示例:
在tittle的示例基础上加上
@allure.title('测试用例2:{login_data}')
@pytest.mark.parametrize('login_data', login_data)
@allure.link('https://www.baid.com')
@allure.link('sss', name='跳转链接1:有name')
@allure.issue('aaa', name='Bug单地址')
@allure.testcase('ddd',name='测试用例地址')
def test_02(self, login_data):
"""测试用例2执行了"""
print(f'用户名:{login_data["user"]}, 密码{login_data["pwd"]}')
run_step_1()
run_step_2()
if login_data['pwd']:
assert True
else:
assert False
效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sVg27htw-1608128106389)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201211230247716.png)]
10) allure 的标记装饰器
1.BDD标记装饰器
提供了三个装饰器
- **@allure.epic:**敏捷里面的概念,定义史诗,往下是 feature
- **@allure.feature:**功能点的描述,理解成模块往下是 story
- **@allure.story:**故事,往下是 title
示例:
# test_demo.py
import pytest
import allure
import os
@allure.epic("XXX模块")
@allure.feature("特性1")
class TestO1:
@allure.story("故事1")
@allure.title('测试用例1')
def test_01(self):
"""测试用例1执行了"""
pass
@allure.story("故事2")
@allure.title('测试用例2')
def test_02(self):
"""测试用例2执行了"""
pass
@allure.epic("XXX模块")
@allure.feature("特性2")
class TestO2:
@allure.story("故事3")
@allure.title('测试用例3')
def test_03(self):
"""测试用例3执行了"""
pass
@allure.story("故事4")
@allure.title('测试用例4')
def test_04(self):
"""测试用例4执行了"""
pass
if __name__ == '__main__':
pytest.main(["-v", "--alluredir=Report/Allure/AllureResult", "--clean-alluredir"])
os.system("allure generate -o Report/Allure/AllureResult")
os.system("allure serve Report/Allure/AllureResult")
效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LZ4xbqSM-1608128106390)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201212222022693.png)]
从上面的效果来看通过BDD标记装饰器,可以很好的分类用例脚本,当然跟 @pytest.mark.xxx 指定标签运行的方式一样,添加下面的命令行参数,也能分类执行测试用例,效果的话,自行体会。
- –allure-epics
- –allure-features
- –allure-stories
2.用例优先(问题严重)级别标记
平时写测试用例也会划分优先级,同样allure 也提供用例级别,在 报告可以清晰看到不同级别用例的缺陷数量
- blocker:阻塞缺陷(功能未实现,无法下一步)
- critical:严重缺陷(功能点缺失)
- normal: 一般缺陷(边界情况,格式错误)
- minor:次要缺陷(界面错误与ui需求不符)
- trivial: 轻微缺陷(必须项无提示,或者提示不规范)
示例:
# test_demo.py
import pytest
import allure
import os
@allure.epic("XXX模块")
@allure.feature("特性1")
class TestO1:
@allure.story("故事1")
@allure.title('测试用例1')
@allure.severity('blocker')
def test_01(self):
"""测试用例1执行了"""
assert False
@allure.story("故事2")
@allure.title('测试用例2')
@allure.severity('blocker')
def test_02(self):
"""测试用例2执行了"""
pass
@allure.epic("XXX模块")
@allure.feature("特性2")
class TestO2:
@allure.story("故事3")
@allure.title('测试用例3')
@allure.severity('critical')
def test_03(self):
"""测试用例3执行了"""
assert False
@allure.story("故事4")
@allure.title('测试用例4')
@allure.severity('normal')
def test_04(self):
"""测试用例4执行了"""
pass
if __name__ == '__main__':
pytest.main(["-v", "--alluredir=Report/Allure/AllureResult", "--clean-alluredir"])
os.system("allure generate -o Report/Allure/AllureResult")
os.system("allure serve Report/Allure/AllureResult")
效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3wJG4WKq-1608128106390)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201212223537216.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXHLtUjV-1608128106391)(/Users/zhangyuxun/Library/Application Support/typora-user-images/image-20201212223603108.png)]
当然也可以和BDD一样,也能通过命令行按优先级执行用例
- 命令行参数加: --allure-severities
十、Pytest 的钩子函数(Hooks)
1)pytest_runtest_makereport
这个钩子方法可以更清晰的了解用例的执行过程,并获取到每个用例的执行结果。
这里item是测试用例,call是测试步骤,具体执行过程如下:
- 先执行when=‘setup’ 返回setup 的执行结果
- 然后执行when=‘call’ 返回call 的执行结果
- 最后执行when='teardown’返回teardown 的执行结果
示例:
# conftest.py
import pytest
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
print('------------------------------------')
# 获取钩子方法的调用结果
out = yield
print('用例执行结果', out)
# 3. 从钩子方法的调用结果中获取测试报告
report = out.get_result()
print('测试报告:%s' % report)
print('步骤:%s' % report.when)
print('nodeid:%s' % report.nodeid)
print('description:%s' % str(item.function.__doc__))
print(('运行结果: %s' % report.outcome))
# test_demo.py
import pytest
import os
class TestO1:
@pytest.fixture(autouse=True)
def login(self):
print('执行前')
yield
print('执行后')
@pytest.mark.usefixtures('login')
def test_01(self, login):
"""测试用例1执行了"""
assert False
if __name__ == '__main__':
pytest.main(["-v"])
运行结果:
collecting ... collected 1 item
test_demo_1.py::TestO1::test_01 ------------------------------------
用例执行结果 <pluggy.callers._Result object at 0x1032af750>
测试报告:<TestReport 'test_demo_1.py::TestO1::test_01' when='setup' outcome='passed'>
步骤:setup
nodeid:test_demo_1.py::TestO1::test_01
description:测试用例1执行了
运行结果: passed
执行前
------------------------------------
用例执行结果 <pluggy.callers._Result object at 0x1032afb90>
测试报告:<TestReport 'test_demo_1.py::TestO1::test_01' when='call' outcome='failed'>
步骤:call
nodeid:test_demo_1.py::TestO1::test_01
description:测试用例1执行了
运行结果: failed
FAILED [100%]
test_demo_1.py:14 (TestO1.test_01)
self = <TestCase.test_demo_1.TestO1 object at 0x1032a2d10>, login = None
@pytest.mark.usefixtures('login')
def test_01(self, login):
"""测试用例1执行了"""
> assert False
E assert False
test_demo_1.py:18: AssertionError
执行后
断言失败
------------------------------------
用例执行结果 <pluggy.callers._Result object at 0x1032bdbd0>
测试报告:<TestReport 'test_demo_1.py::TestO1::test_01' when='teardown' outcome='passed'>
步骤:teardown
nodeid:test_demo_1.py::TestO1::test_01
description:测试用例1执行了
运行结果: passed