Pytest测试实战
一、使用约束
在执行pytest命令时,会自动从当前目录及子目录中寻找符合以下约束的测试函数来执行。可以在pytest.ini中修改测试目录、测试模块、测试类、测试方法扫描进行默认修改。
-
在单测文件中,所有测试类以Test开头,并且不能带’init’方法
-
在单测类中,可以包含一个或者多个以’test_'开头的函数
二、常用命令
- pytest -v test_xxx.py —> -v参数可以得到更完整的前后对比信息。
- pytest -collect-only test_xxx.py —> 用来显示给定配置下会有哪些测试用例被运行。也可以用**’–co’**参数。
- pytest -k “add or passing” --co —> 使表达式指定特定的测试用例运行。
- pytest -m test_xxx.py —> 该选项用于运行做过标记的用例。标记使用装饰器:@pytest.mark。
- pytest -x test_xxx.py —> 用于断言失败或者触发了异常时立即停止整个会话。方便调试。
- pytest –maxfail=1 test_xxx.py —> 指定失败几次后停止,num等于1时相当于-x。
- pytest -s test_xxx.py —> 允许测试时输出任何符合标准的输出流信息,例如print。
- pytest -lf test_xxx.py —> 用于重新运行之前失败的用例,如果没有任何失败,则全部重跑。
- pytest -ff test_xxx.py —> 先运行失败的用例再运行其他的。
- pytest -q test_xxx.py —> 与 -v 相反,安静模式,只显示关键的信息。
- pytest -l test_xxx.py —> 用来失败时显示局部变量。
- pytest –duration test_xxx.py —> 用来显示哪个阶段占用时间最长。
- pytest --setup-show test_xxx.py —> 用来查看fixture的执行过程。
三、标记操作
-
@pytest.mark.skip(reason=’’) —> 跳过一些不想执行的测试用例。
-
@pytest.mark.skipif(version==5, reason=’’) —> 满足某个条件时跳过一些不想执行的测试用例。
-
@pytest.mark.xfail() —> 用来标记一些预计原本就不会执行通过的测试用例,如果预计失败运行失败,显示x,预计失败运行成功,显示X。
-
@pytest.mark.parametrize(“name”, [“Jone”, “Mark”]) —> 用来参数化测试
@pytest.mark.parametrize("name, age", [("Mark", 18), ("Joe", 20)]) def test_user_name(name, age): assert name == "Joe" and age == 20
-
@pytest.mark.usefixtures(‘fixture1’, ‘fixture2’) 标记测试函数或者类。使用usefixtures需要在参数列表中指定一个或者多个fixture字符串。非常适合测试类。
四、组织测试用例合集
- 执行某个目录下的单元测试:cd path && pytest
- 执行单个文件:pytest test_xx.py
- 执行单个测试类:pytest test_xx.py::TestClass
- 执行某个测试类中的某个用例:pytest test_xx.py::TestClass::test_class_01
五、fixture
fixture是在测试函数运行前后,由pytest执行的外壳函数。fixture中的代码可以定制,满足多变的测试需求,包括定义传入测试中的数据集、配置测试前系统的初始状态、为批量测试提供数据源等。
**@pytest.fixture()**用于指定函数是一个fixture。如果测试函数的参数列表中包含fixture名,那么会被pytest检测到,并在函数运行之前运行该fixture。fixture可以完成任务,也可以返回数据给测试函数。
"""使用fixture执行配置及销毁逻辑"""
import pytest
@pytest.fixture()
def some_data():
"""Return answer to ultimate question"""
return 42
def test_some_data(some_data):
"""User fixture return value in a test"""
assert some_data == 42
# 使用pytest test_task.py::test_some_data --setup-show 命令执行以上测试用例,会看到如下结果,其中fixture名称前的F和S代表的是fixture的作用范围,F代表函数级别,S代表会话级别。
'''
========================================================================================== test session starts ==========================================================================================
platform win32 -- Python 3.6.8, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: C:\Users\yuyy\PycharmProjects\djangoProject\tests, configfile: pytest.ini
plugins: html-3.1.1, metadata-1.11.0
collected 1 item
test_task.py
SETUP F some_data
unit/test_task.py::test_some_data (fixtures used: some_data).
TEARDOWN F some_data
=========================================================================================== 1 passed in 0.03s ===========================================================================================
'''
fixture包含一个叫scope的可选参数,用于控制fixture执行配置和销毁逻辑的频率。它有4个待选值:function、class、module、session(默认值为session)。
· scope=‘function’ —> 函数级别的fixture每个测试函数只需要运行一次。配置代码在测试用例运行之前运行,销毁代码在测试用例运行之后运行。function是scope参数的默认值。
· scope=‘class’ —> 类级别的fixture每个测试类只需要运行一次,无论测试类里有多少类方法都可以共享这个fixture。
· scope=‘module’ —> 模块级别的fixture每个模块只需要运行一次,无论模块里有多少个测试函数、类或者其他fixture都可以共享这个fixture。
· scope=‘session’ —> 会话级别的fixture每次会话只需要运行一次。一次pytest会话中的所有测试函数、方法都可以共享这个fixture。
使用示例如下:
import pytest
@pytest.fixture(scope='class')
def class_scope():
print("a class scope.")
@pytest.fixture(scope='module')
def module_scope():
print("a module scope.")
fixture作为测试函数的参数,也会被堆栈跟踪并纳入测试报告。如果fixture发生异常,则使用该fixture的测试用例会报ERROR而不是FAIL,这样用户就知道失败不是发生在核心测试函数内,而是在测试依赖的fixture。fixture传递数据的应用示例如下:
"""使用fixture来传递数据"""
import pytest
@pytest.fixture()
def a_tuple():
return 1, 'foo', None, {'bar': 23}
def test_a_tuple(a_tuple):
assert a_tuple[3]['bar'] == 32
使用usefixtures和在测试方法中添加fixture参数,二者大体上是差不多的。区别之一在于只有后者才能够使用fixture的返回值。使用示例如下:
import pytests
@pytests.mark.usefixtures('use_one', 'use_two')
class TestSomething():
def test_1():
assert 1 == 1
@pytest.fixture()
def use_one(scope='class'):
print("class fixture 1")
@pytest.fixture()
def use_two(scope='class'):
print("class fixture 2")
fixture的名字展示在使用它的测试或者其他fixture函数的参数列表上,通常和fixture函数名保持一致,但是也可以使用name参数对fixture重新命名。示例如下:
import pytest
@pytest.fixture(name='lue')
def ultimate_answer_to_lift_the_universe_and_everything():
return 'love'
def test_everything(lue):
assert lue == 'love'
pytest有一些内置的fixture,比如tmpdir、tmpdir_factory。其应用示例如下:
import pytest
# 使用内置的fixturetmpdir,它的返回值是py.path.local类型的一个对象。它只能作用于函数级别,所以只能针对测试函数使用tmpdir创建文件或目录
# 如果需要fixture作用范围高于函数级别,应使用tmpdir_factory
def test_tmpdir(tmpdir):
a_file = tmpdir.join("something.txt")
a_sub_dir = tmpdir.mkdir('anything')
another_file = a_sub_dir.join('something_else.txt')
a_file.write("contents may settle during shipping.")
another_file.write("something different")
assert a_file.read() == 'contents may settle during shipping.'
assert another_file.read() == 'something different'
# tmpdir_factory的作用范围是会话级别的,如果需要模块或者类级别作用范围目录,可以利用tmpdir_factory再创建一个fixture
def test_tmpdir_factory(tmpdir_factory):
a_dir = tmpdir_factory.mktemp("mydir") # 创建a_dir目录,在此函数内,它的效果与使用tmpdir是一样的。
base_temp = tmpdir_factory.getbasetemp() # getbasetemp()函数返回了该会话使用的根目录。
print("base: ", base_temp)
a_file = a_dir.join('something.txt')
a_sub_dir = a_dir.mkdir('anything')
another_file = a_sub_dir.join('something_else.txt')
a_file.write("contents may settle during shipping.")
another_file.write("something different")
assert a_file.read() == 'contents may settle during shipping.'
assert another_file.read() == 'something different'
@pytest.fixture(scope='module')
def author_file_json(tmpdir_factory):
"""
创建一个临时目录data,并在此目录下创建了一个author_file.json。然后将python_author_data字典数据写入JSON文件中。
"""
python_author_data = {
'Ned': {'City': 'Boston'},
'Brian': {'City': 'Portland'}
}
file = tmpdir_factory.mktemp('data').join('author_file.json')
with file.open('w') as f:
json.dump(python_author_data, f)
return file
def test_brain_in_poland(author_file_json):
with author_file_json.open() as f:
authors = json.loads(f)
assert authors['Brain']['City'] == 'Portland'
def test_all_have_cities(author_file_json):
with author_file_json.open() as f:
authors = json.loads(f)
for a in authors:
assert len(authors[a]['City']) > 0
六、Exit Code含义
-
Exit code 0:所有用例执行完毕,全部通过
-
Exit code 1:所有用例执行完毕,存在Failed的测试用例
-
Exit code 2:用户中断了测试执行
-
Exit code 3:测试过程中发生了内部错误
-
Exit code 4:pytest 命令行使用错误
-
Exit code 5:未采集到可用测试用例文件