Pytest测试框架
基本介绍
- 介绍:pytest是python的一种单元测试框架,同自带的Unittest测试框架类似,相比于Unittest框架使用起来更简洁,效率更高。
- 特点
1. 易上手,入门简单
2. 支持简单单元测试和复杂的功能测试
3. 支持参数化
4. 执行测试过程中可以将某些测试跳过,或者对某些预期失败的case标记成失败
5. 支持重复执行失败的case
6. 支持运行由Nose,Unittest编写的测试Case
7. 具有第三方插件,可以自定义扩展
8. 方便和持续集成工具集成
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
- 标记式表达式(将会运行用@ pytest.mark.slow装饰器修饰的所有测试) pytest -m slow
- 遇到错误停止测试 pytest -x test_class.py
- 错误达到指定数量停止测试 pytest --maxfail=1
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来完成多个用例
使用@pytest.fixture(scope="function",params=None,autouse=False,ids=None,name=None):装饰器来标记fuxture的功能 - 使用此装饰器(带或者不带参数)来定义fixture功能,fixture功能的名称可以在以后使用
- 引用它会在运行测试之前调用它:test模块或类可以使用pytest.mark.usefixtures
- 测试功能可以直接使用fixture名称作为输入参数,在这种情况下夹具实例从fixture返回功能将被注入
- scope scope有四个级别参数
1. 默认的function 每一个函数或方法都会调用
2. class 每一个类调用一次
3. module 每一个.py文件调用一次,该文件内有多个function和class
4. session 多个文件调用一次,可以跨.py文件调用个,每个.py文件就是module - params 一个可选的参数列表,会导致多个参数fixture功能和所有测试使用它
- autouse 如果为True,则为所有测试激活fixture func可以看到它,如果为False(默认值),则需要显示激活fixture
- ids 每个字符串id的列表,每个字符串对应与params这样他们就是测试ID的一部分,如果没有提供ID它们将从params自动生成
- yield控制teardown
1. 编写用例时,将yield写在结束操作之前即可,然后在所有用例执行完之后执行一次
2. yield在用例执行过程中,用例出现异常,不影响yield后面的teardown执行,运行结果互不影响,并且全部用例执行完以后,yield呼唤teardown操作
3. 在setup就异常了,那么是不会去执行yield后面的teardown内容
fixture之scope参数常规使用
使用@pytest.fixture(scope="module"):module作用是当前整个py文件都能生效,参数写上函数名称即可
# 新建一个文件sdfdsf.py
# coding:utf-8
import pytest
@pytest.fixture(scope="module")
def open():
print("打开浏览器,并且打开百度首页")
def test_s1(open):
print("用例1:搜索python-1")
def test_s2(open):
print("用例2:搜索python-2")
def test_s3(open):
print("用例3:搜索python-3")
if __name__ == "__main__":
pytest.main(["-s", "sdfdsf.py"])
通过结果可以看出,三个测试用例都调用了open函数,但是只会在第一个用例之前执行一次
通过规律可以得出,如果我第一个测试用例之前不调用,那么我就在第二个测试用例调用open函数
scope控制setup,yield控制teardown实例
# 新建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还是会正常执行不会被影响
Pytest之autouse=True
平常写自动化用例会写一些前置的fixture操作,用例需要用到就直接传该函数的参数名称就行了,当用例很多的时候,每次都传这个参数,会比较麻烦,fixture里面有个参数autouse,默认是Fasle没开启的,可以设置为True开启自动使用fixture功能,这样用例就不用每次都去传参了
调用fixture三种方法:
- 函数或类里面方法直接传fixture的函数参数名称
- 使用装饰器@pytest.mark.userfixtures()修饰
- autouse=True自动使用
方式一:用例传fixture参数
先定义start功能,用例全部传start参数,调用该功能
# content of test_06.py
import time
import pytest
@pytest.fixture(scope="function")
def start(request):
print('
-----开始执行function----')
def test_a(start):
print("-------用例a执行-------")
class Test_aaa():
def test_01(self, start):
print('-----------用例01--------------')
def test_02(self, start):
print('-----------用例02------------')
if __name__ == "__main__":
pytest.main(["-s", "sdfg.py"])
方式二:装饰器usefixtures
使用装饰器@pytest.mark.usefixtures()修饰需要运行的用例
# content of test_07.py
import time
import pytest
@pytest.fixture(scope="function")
def start(request):
print('
-----开始执行function----')
@pytest.mark.usefixtures("start")
def test_a():
print("-------用例a执行-------")
@pytest.mark.usefixtures("start")
class Test_aaa():
def test_01(self):
print('-----------用例01--------------')
def test_02(self):
print('-----------用例02------------')
if __name__ == "__main__":
pytest.main(["-s", "sdfg.py"])
方式三:设置autouse=True
autouse设置为True,自动调用fixture功能
- start设置scope为module级别,在当前.py用例模块只执行一次,autouse=True自动使用
- open_home设置scope为function级别,每个用例前都调用一次,自动使用
# content of test_08.py
import time
import pytest
@pytest.fixture(scope="module")
def start(request):
print('
-----开始执行moule----')
print('module : %s' % request.module.__name__)
print('----------启动浏览器---------')
yield
print("------------结束测试 end!-----------")
@pytest.fixture(scope="function", autouse=True)
def open_home(request):
print("function:%s
--------回到首页--------" % request.function.__name__)
def test_01():
print('-----------用例01--------------')
def test_02():
print('-----------用例02------------')
if __name__ == "__main__":
pytest.main(["-s", "sdfg.py"])
Pytest之fixture传参数request
如果想把登录操作放到前置操作里,也就是用到@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"])
conftest.py配置
在上面的案例中,同一个py文件,多个用例调用一个登陆功能,如果有多个py文件都需要调用这个登陆功能的话,就不能把登录写到用例里面去了,此时应该有个配置文件,单独管理一些预置的操作场景,pytest里面默认读取conftest.py里面的配置
conftest.py配置需要注意一下几点:
- conftest.py配置名称是固定的,不能改名称
- conftest.py与运行的用例要在同一个package下,并且有__init__.py文件
- 不需要import导入conftest.py,pytest用例会自动查找
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-rerunfailures插件,使用pip安装就行 pip install pytest-rerunfailures
- 使用方法:命令行格式 pytest --reruns n n:表示为重试的次数 这种命令行一般放在pytest.ini的配置文件中,在addopts中的后面添加命令行参数
- 用例失败再重跑1次,命令行加个参数--reruns 1 py.test --reruns 1 --html=report.html --self-contained-html
Pytest控制函数顺序
函数修饰符的方式标记被测函数执行的顺序
安装方式:
插件名称:使用命令行 pip3 install pytest-ordering
使用方法:
标记于被测试函数,@pytest.mark.run(order=x)
order的优先级 0>较小的正数>较大的正数>无标记>较小的负数>较大的负数
根据order传入的参数来结局运行顺序
order值全为证书或权威负数时,运行顺序:值越小,优先级越高
正数和负数同时存在:正数优先级高
默认情况下,pytest默认从上往下执行,可以通过第三方插件包改变其运行顺序.
Pytest跳过测试函数
- @pytest.mark.skip可以标记无法在某些平台上运行的测试功能,或者希望失败的测试功能,
- skip意味着只有在满足某些条件时才希望测试通过,否则pytest应该跳过运行测试。 常见示例是在非Windows平台上跳过仅限Windows的测试,或跳过测试依赖于当前不可用的外部资源(例如数据库)。
- xfail意味着您希望测试由于某种原因而失败。 一个常见的例子是对功能的测试尚未实施,或尚未修复的错误。 当测试通过时尽管预计会失败(标有pytest.mark.xfail),它是一个xpass,将在测试摘要中报告。
- pytest计数并分别列出skip和xfail测试。
skip
跳过测试函数的最简单方法是使用跳过装饰器标记它,可以传递一个可选的原因
@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown():
...
也可以通过调用来测试执行或设置期间强制跳过pytest.skip(reason)功能:
def test_function(): if not valid_config(): pytest.skip("unsupported configuration")
也可以使用pytest.skip(reason,allow_module_level = True)跳过整个模块级别
import pytest
if not pytest.config.getoption("--custom-flag"):
pytest.skip("--custom-flag is missing, skipping tests", allow_module_level=True)
skipif
根据特定的条件,不执行标识的测试函数
方法:skipif(condition,reason=None)
参数:
condition:跳过的条件,必传参数 可以直接传True
reason:标注原因,必传参数 可以传reason="done"表示正在执行跳过
使用方法:@pytest.mark.skipif(condition,reason="xxx")
Pytest之error和failed区别
测试结果一般分为三种
- passed(通过)
- failed(一般在测试用例里面断言失败)
- error(一般在fixture断言失败或者自己代码写的有问题)
Pytest标记为预期失败函数
当用例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里面用xfail标记用例为失败的用例,可以直接跳过,实现基本思路
- 把登录写为前置操作
- 对登录的账户和密码参数化,参数用参数=[{"user":"admin","pwd":"111"}]表示
- 多个用例放在一个Test_xx的class里
- test_01,test_02,test_03全部调用fixture里面的login功能
- test_01测试登陆用例
- test_02和test_03执行前用if判断登录的结果,登录失败就执行,pytest.xfail("登录不成功,标记为xfail")
登录成功的用例
# content of test_05.py
# coding:utf-8
import pytest
canshu = [{"user": "amdin", "psw": "111"}]
@pytest.fixture(scope="module")
def login(request):
user = request.param["user"]
psw = request.param["psw"]
print("正在操作登录,账号:%s, 密码:%s" % (user, psw))
if psw:
return True
else:
return False
@pytest.mark.parametrize("login", canshu, indirect=True)
class Test_xx():
def test_01(self, login):
'''用例1登录'''
result = login
print("用例1:%s" % result)
assert result == True
def test_02(self, login):
result = login
print("用例2,登录结果:%s" % result)
if not result:
pytest.xfail("登录不成功, 标记为xfail")
assert 1 == 1
def test_03(self, login):
result = login
print("用例3,登录结果:%s" % result)
if not result:
pytest.xfail("登录不成功, 标记为xfail")
assert 1 == 1
if __name__ == "__main__":
pytest.main(["-s", "test_05.py"])
登录失败的用例
# content of test_05.py
# coding:utf-8
import pytest
canshu = [{"user": "amdin", "psw": ""}]
@pytest.fixture(scope="module")
def login(request):
user = request.param["user"]
psw = request.param["psw"]
print("正在操作登录,账号:%s, 密码:%s" % (user, psw))
if psw:
return True
else:
return False
@pytest.mark.parametrize("login", canshu, indirect=True)
class Test_xx():
def test_01(self, login):
'''用例1登录'''
result = login
print("用例1:%s" % result)
assert result == True
def test_02(self, login):
result = login
print("用例3,登录结果:%s" % result)
if not result:
pytest.xfail("登录不成功, 标记为xfail")
assert 1 == 1
def test_03(self, login):
result = login
print("用例3,登录结果:%s" % result)
if not result:
pytest.xfail("登录不成功, 标记为xfail")
assert 1 == 1
if __name__ == "__main__":
pytest.main(["-s", "test_05.py"])
Pytest的参数化
Pytest的常规参数化
单个参数的参数化
方法:parametrize(argnames,argvalues,indirect=False,ids=None,scope=None)
常用参数:
argnames:参数名
argvalues:参数对应值,类型必须为list ["1","2","3"]这种类型
使用方法:@pytest.mark.parametrize(argnames,argvalues) 参数值为Nge,测试方法就会运行N次
单个参数的参数化代码如下:
@pytest.mark.parametrize("keys",["1","2"]) # 第二个列表参数写一个表示执行一个参数一次,两个表示执行一个参数两次
def test_search(self,keys):
self.driver.find_element_by_id("com.android.settings:id/search").click()
self.driver.find_element_by_id("android:id/search_src_text").send_keys(keys)
多个参数的参数化代码如下:
# 第一个参数使用元祖,第二个参数使用列表嵌套元祖
@pytest.mark.parametrize(("username","password"),[("wupeng","123456"),("wupeng1","123456")])
def test_search(self,username,password):
self.driver.find_element_by_id("com.android.settings:id/search").click()
self.driver.find_element_by_id("android:id/search_src_text").send_keys(username)
self.driver.find_element_by_id("android:id/search_src_text").send_keys(password)
第一种生成allure动态标题方式
import pytest
import allure
class TestLogin:
datas = [
({"username": "liuxin", "password": 123456}, "success", "输入正确用户名,正确密码"),
({"username": "liuxin", "password": 123456}, "faild", "输入错误用户名,正确密码"),
({"username": "liuxin", "password": 123456}, "success", "输入正确用户名,正确密码"),
]
@pytest.mark.parametrize("username,password,value", datas)
@allure.title("{value}")
def test_login(self, username, password, value):
result = password
assert result == "success"
if __name__ == '__main__':
pytest.main(["-s", "test_pytest.py"])
第二种生成allure动态标题
- feature模块 allure.dynamic.feature(feature_name)
- 功能点story allure.dynamic.story(case_story)
- 用例标题title allure.dynamic.title(case_title)
- 用例描述 allure.dynamic.description(case_description)
参数ids之unicode问题
在没有配置conftest.py的情况下,终端运行如下代码:
import pytest
def login(username, password):
'''登录'''
# 返回
return {"code": 0, "msg": "success!"}
# 测试数据
test_datas = [
({"username": "yoyo1", "password": "123456"}, "success!"),
({"username": "yoyo2", "password": "123456"}, "success!"),
({"username": "yoyo3", "password": "123456"}, "success!"),
]
@pytest.mark.parametrize("test_input,expected",
test_datas,
ids=[
"输入正确账号,密码,登录成功",
"输入错误账号,密码,登录失败",
"输入正确账号,密码,登录成功",
]
)
def test_login(test_input, expected):
'''测试登录用例'''
# 获取函数返回结果
result = login(test_input["username"], test_input["password"])
# 断言
assert result["msg"] == expected
配置conftest.py
def pytest_collection_modifyitems(items):
"""
测试用例收集完成时,将收集到的item的name和nodeid的中文显示在控制台上
:return:
"""
for item in items:
item.name = item.name.encode("utf-8").decode("unicode_escape")
print(item.nodeid)
item._nodeid = item.nodeid.encode("utf-8").decode("unicode_escape")
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之分布式执行
优点:节约时间
安装插件: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之重复执行用例
平常在做功能测试时,经常遇到某个模块不稳定,偶然会出现一些bug,或者是特定条件需要执行多次用例的,我们需要针对某个某块用例重复执行多次
pytest-repeat是pytest的一个插件,用于重复执行单个用例或多个用例,并指定重复多次
安装及使用
- 下载安装:pip install pytest-repeat
- 使用--count命令指定要运行测试用例和测试次数 py.test --count=10 test_file.py
--repeat-scope
从运行的用例结果看,是运行完其中一个测试用例一个轮回,在运行下一个测试用例一个轮回,但是希望是当前文件中的所有测试用例执行一遍后,在继续重复执行
--repeat-scope类似于pytest fixture的scope参数,--repeat-scope也可以设置参数:
- function 默认范围针对每个用例重复执行,在执行下一个用例
- class 以class为用例集合单位,重复执行class里面的用例在执行下一个
- module 以模块为单位,重复执行模块里面的用例,在执行下一个
- session 重复这个测试会话,即所有收集的测试执行一次,然后所有这些测试在此执行等等
使用--repeat-scope=session重复执行整个会话用例 pytest baidu/test_1_baidu.py -s --count=5 --repeat-scope=session
@pytest.mark.repeat(count)
如果在代码中需要标记重复多次的测试,可以使用@pytest.mark.repeat(count)装饰器
Pytest之pytest-assume
pytest断言失败后,后面的代码就不会执行了,通常一个用例会写多个断言,有时候我们希望第一个断言失败后,后面能继续断言,那么pytest-assume插件可以解决断言失败后继续断言的问题
安装 pip install pytest-assume
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("测试完成!")
Pytest的配置文件
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*
logging日志
基本操作:在项目根目录下创建一个log目录,log目录下创建log文件,在需要使用logging日志的时候,引入logger
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
log.py
"""
import os
import logging
import logging.handlers
__all__ = ["logger"]
# 用户配置部分 ↓
LEVEL_COLOR = {
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
}
STDOUT_LOG_FMT = "%(log_color)s[%(asctime)s] [%(levelname)s] [%(threadName)s] [%(filename)s:%(lineno)d] %(message)s"
STDOUT_DATE_FMT = "%Y-%m-%d %H:%M:%S"
FILE_LOG_FMT = "[%(asctime)s] [%(levelname)s] [%(threadName)s] [%(filename)s:%(lineno)d] %(message)s"
FILE_DATE_FMT = "%Y-%m-%d %H:%M:%S"
# 用户配置部分 ↑
class ColoredFormatter(logging.Formatter):
COLOR_MAP = {
"black": "30",
"red": "31",
"green": "32",
"yellow": "33",
"blue": "34",
"magenta": "35",
"cyan": "36",
"white": "37",
"bg_black": "40",
"bg_red": "41",
"bg_green": "42",
"bg_yellow": "43",
"bg_blue": "44",
"bg_magenta": "45",
"bg_cyan": "46",
"bg_white": "47",
"light_black": "1;30",
"light_red": "1;31",
"light_green": "1;32",
"light_yellow": "1;33",
"light_blue": "1;34",
"light_magenta": "1;35",
"light_cyan": "1;36",
"light_white": "1;37",
"light_bg_black": "100",
"light_bg_red": "101",
"light_bg_green": "102",
"light_bg_yellow": "103",
"light_bg_blue": "104",
"light_bg_magenta": "105",
"light_bg_cyan": "106",
"light_bg_white": "107",
}
def __init__(self, fmt, datefmt):
super(ColoredFormatter, self).__init__(fmt, datefmt)
def parse_color(self, level_name):
color_name = LEVEL_COLOR.get(level_name, "")
if not color_name:
return ""
color_value = []
color_name = color_name.split(",")
for _cn in color_name:
color_code = self.COLOR_MAP.get(_cn, "")
if color_code:
color_value.append(color_code)
return " 33[" + ";".join(color_value) + "m"
def format(self, record):
record.log_color = self.parse_color(record.levelname)
message = super(ColoredFormatter, self).format(record) + " 33[0m"
return message
def _get_logger(log_to_file=True, log_filename="default.log", log_level="DEBUG"):
_logger = logging.getLogger(__name__)
stdout_handler = logging.StreamHandler()
stdout_handler.setFormatter(
ColoredFormatter(
fmt=STDOUT_LOG_FMT,
datefmt=STDOUT_DATE_FMT,
)
)
_logger.addHandler(stdout_handler)
if log_to_file:
_tmp_path = os.path.dirname(os.path.abspath(__file__))
_tmp_path = os.path.join(_tmp_path, "../log/{}".format(log_filename))
file_handler = logging.handlers.TimedRotatingFileHandler(_tmp_path, when="midnight", backupCount=30,encoding="utf-8")
file_formatter = logging.Formatter(
fmt=FILE_LOG_FMT,
datefmt=FILE_DATE_FMT,
)
file_handler.setFormatter(file_formatter)
_logger.addHandler(file_handler)
_logger.setLevel(log_level)
return _logger
logger = _get_logger(log_to_file=True)