fixture固定装置,是pytest用于将测试前后进行预备、清理工作的代码分离出核心测试逻辑的一种机制。
《pytest测试实战》学习并进行整理输出,所以是依照书中的例子进行学习和实践。
代码下载路径
链接:https://pragprog.com/titles/bopytest/python-testing-with-pytest/
一、fixture存放位置
1.fixture可以定义在测试文件中
2.定义在conftest.py中,供所在目录及子目录下的测试使用
尽管conftest.py是Python模块,但它不能被测试文件导入。import conftest用法是不允许出现的。conftest.py被pytest视作一个本地插件库。
二、fixture使用
1.使用fixture执行配置及销毁逻辑
测试用例中需要预先完成Tasks数据库的配置及启动,以及测试完毕后清理以及断开数据库连接。
import pytest
import tasks
from tasks import Task
@pytest.fixture()
def tasks_db(tmpdir):
"""connect to db before tests,disconnect after."""
# Setup : start db
tasks.start_tasks_db(str(tmpdir), 'tiny')
yield # this is where the testing happens
# Teardown : stop db
tasks.stop_tasks_db()
当测试函数引用此fixture后,那么运行测试用例之前会运行tasks.start_tasks_db(str(tmpdir), ‘tiny’),当运行到yield时,系统将停止fixture的运行,转而运行测试函数,等测试函数运行完成之后再回到fixture运行yield之后的代码。
2.使用–setup-show回溯fixture的执行过程
使用–setup-show可以看到测试过程中执行的什么,以及执行的先后顺序。
fixture前面的F和S代表了fixture的作用范围。F代表函数级别的作用范围,S代表会话级别的作用范围
3.使用fixture传递测试数据
fixture非常适合存放测试数据,并且它能返回任何数据类型。下面代码中fiixture函数a_tuple=(1, ‘foo’, None, {‘bar’: 23})
test_a_tuple测试函数对a_tuple中的数据进行断言
@pytest.fixture()
def a_tuple():
"""Return something more interesting."""
return (1, 'foo', None, {'bar': 23})
def test_a_tuple(a_tuple):
"""Demo the a_tuple fixture."""
assert a_tuple[3]['bar'] == 32
当fixture内部出现错误时,会报ERROR而不是FAIL,用户就会知道失败不是发生在核心测试函数中,而是发生在测试所依赖的fixture
4.使用多个fixture
下面为tasks_proj/tests/conftest.py代码,编写了四个fixture。
前两个fixture返回了包含多个属性task的对象数据。
后两个fixture的形参中各有两个fixture(tasks_db和返回数据集的fixture),数据集用于向数据库中添加task数据记录,这样测试时就可以使用测试库数据,而非空数据库。
@pytest.fixture()
def tasks_just_a_few():
"""All summaries and owners are unique."""
return (
Task('Write some code', 'Brian', True),
Task("Code review Brian's code", 'Katie', False),
Task('Fix what Brian did', 'Michelle', False))
@pytest.fixture()
def tasks_mult_per_owner():
"""Several owners with several tasks each."""
return (
Task('Make a cookie', 'Raphael'),
Task('Use an emoji', 'Raphael'),
Task('Move to Berlin', 'Raphael'),
Task('Create', 'Michelle'),
Task('Inspire', 'Michelle'),
Task('Encourage', 'Michelle'),
Task('Do a handstand', 'Daniel'),
Task('Write some books', 'Daniel'),
Task('Eat ice cream', 'Daniel'))
@pytest.fixture()
def db_with_3_tasks(tasks_db, tasks_just_a_few):
"""Connected db with 3 tasks, all unique."""
for t in tasks_just_a_few:
tasks.add(t)
@pytest.fixture()
def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner):
"""Connected db with 9 tasks, 3 owners, all with 3 tasks."""
for t in tasks_mult_per_owner:
tasks.add(t)
位于tasks_proj/tests/func/test_add.py中的测试函数,使用fixture函数db_with_3_tasks初始化数据库并包含三条数据,使用tasks.add()添加一条数据后,判断数据库中tasks对象的总量是否为4.
def test_add_increases_count(db_with_3_tasks):
"""Test tasks.add() affect on tasks.count()."""
# GIVEN a db with 3 tasks
# WHEN another task is added
tasks.add(Task('throw a party'))
# THEN the count increases by 1
assert tasks.count() == 4
运行结果:
pytest --setup-show ./tests/func/test_add.py::test_add_increases_count
5.制定fixture作用范围
fixture包含了一个scope的可选参数,用于控制fixture执行配置和销毁逻辑的频率。@pytest.fixture()的scope参数有四个待选值:function、class、module、session(默认值为function)
- scope=‘function’
函数级别的fixture每个测试函数只需要运行一次。配置代码在测试用例运行之前运行,销毁代码在测试用例运行后运行。 - scope=‘class’
类级别的fixture每个测试类只需要运行一次。无论测试类里有多少个类方法,都可以共享这个fixture。 - scope=‘module’
模块级别的fixture每个模块只需要运行一次。无论模块里有多少个测试函数、类方法或其他fixture都可以共享这个fixture。 - scope=‘session’
会话级别的fixture每次会话只需要运行一次。一次pytest会话中的所有测试函数、方法都可以共享这个fixture。
"""Demo fixture scope."""
import pytest
@pytest.fixture(scope='function')
def func_scope():
"""A function scope fixture."""
@pytest.fixture(scope='module')
def mod_scope():
"""A module scope fixture."""
@pytest.fixture(scope='session')
def sess_scope():
"""A session scope fixture."""
@pytest.fixture(scope='class')
def class_scope():
"""A class scope fixture."""
def test_1(sess_scope, mod_scope, func_scope):
"""Test using session, module, and function scope fixtures."""
def test_2(sess_scope, mod_scope, func_scope):
"""Demo is more fun with multiple tests."""
@pytest.mark.usefixtures('class_scope')
class TestSomething():
"""Demo class scope fixtures."""
def test_3(self):
"""Test using a class scope fixture."""
def test_4(self):
"""Again, multiple tests are more fun."""
执行结果:
注:
1.scope参数是在定义fixture时定义的,而不是在调用fixture时定义的。因此,使用fixture的测试函数是无法改变fixture的作用范围的
2.fixture只能使用同级别的fixture,或比自己级别更高的fixture。譬如函数级别的fixture可以使用同级别的fixture,也可以使用类级别、模块级别、会话级别的fixture,反之则不行。
6.使用usefixture指定fixture
之前都是在测试函数的参数列表中制定fixture,也可以使用@pytest.mark.usefixtures(‘fixture1’,‘fixture2’)标记测试函数或类。这对测试函数来说意义不大,非常适合测试类。
test_scope.py内容:
@pytest.mark.usefixtures('class_scope')
class TestSomething():
"""Demo class scope fixtures."""
def test_3(self):
"""Test using a class scope fixture."""
def test_4(self):
"""Again, multiple tests are more fun."""
使用usefixtures和在测试方法中添加fixture参数,两者大体上差不多。区别之一在于只有后者才能使用fixture的返回值
7.为常用fixture添加autouse选项
通过指定autouse=True选项,使作用域内的测试函数都运行该fixture。
"""Demonstrate autouse fixtures."""
import pytest
import time
@pytest.fixture(autouse=True, scope='session')
def footer_session_scope():
"""Report the time at the end of a session."""
yield
now = time.time()
print('--')
print('finished : {}'.format(time.strftime('%d %b %X', time.localtime(now))))
print('-----------------')
@pytest.fixture(autouse=True)
def footer_function_scope():
"""Report test durations after each function."""
start = time.time()
yield
stop = time.time()
delta = stop - start
print('\ntest duration : {:0.3} seconds'.format(delta))
def test_1():
"""Simulate long-ish running test."""
time.sleep(1)
def test_2():
"""Simulate slightly longer test."""
time.sleep(1.23)
执行结果:
8.为fixture重命名
使用@pytest.fixture()的name参数对fixture进行重命名。
test_rename_fixture.py代码:
"""Demonstrate fixture renaming."""
import pytest
@pytest.fixture(name='lue')
def ultimate_answer_to_life_the_universe_and_everything():
"""Return ultimate answer."""
return 42
def test_everything(lue):
"""Use the shorter name."""
assert lue == 42
运行结果:
pytest指定–fixtures命令行选项,可以查出fixture在哪个文件定义的,并且给出fixture定义时给出的定义内容(""" “”""中的内容)
命令:
D:\wy_only\tasks_proj\tests\func>pytest --fixtures test_rename_fixture.py
注:我在运行此命令时,返回的是所有pytest内置的fixture、conftest.py中的fixture,以及自身调用的fixture。
9.*fixture的参数化
fixture做参数化处理,@pytest.fixture()中使用参数params=tasks_to_try,tasks_to_try可以是对象列表。test_add_variety2.py代码如下:
"""Test the tasks.add() API function."""
import pytest
import tasks
from tasks import Task
tasks_to_try = (Task('sleep', done=True),
Task('wake', 'brian'),
Task('breathe', 'BRIAN', True),
Task('exercise', 'BrIaN', False))
task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)
for t in tasks_to_try]
def equivalent(t1, t2):
"""Check two tasks for equivalence."""
return ((t1.summary == t2.summary) and
(t1.owner == t2.owner) and
(t1.done == t2.done))
@pytest.fixture(params=tasks_to_try)
def a_task(request):
"""Using no ids."""
return request.param
def test_add_a(tasks_db, a_task):
"""Using a_task fixture (no ids)."""
task_id = tasks.add(a_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, a_task)
其中fixture函数a_task的参数request也是pytest内建fixture之一。代表fixture的调用状态,它有一个param字段。会被@pytest.fixture(params=tasks_to_try)的params列表中的一个元素填充。即a_task返回的request.param是一个Task对象(如:Task(‘sleep’, done=True),这里比较难理解)。
其实a_task的逻辑是仅以request.param作为返回值供测试使用。因为task对象列表(tasks_to_try)包含了四个task对象,所以a_task将被测试调用4次。
运行结果:
由于未指定id,pytest用fixture名+一串数字作为task标识。可以在@pytest.fixture()中使用ids参数进行id设置,但是ids必须是一串字符串列表。task_ids定义了字符串列表的格式Task({},{},{}),譬如:Task(sleep,None,True)
task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)
for t in tasks_to_try]
@pytest.fixture(params=tasks_to_try, ids=task_ids)
def b_task(request):
"""Using a list of ids."""
return request.param
def test_add_b(tasks_db, b_task):
"""Using b_task fixture, with ids."""
task_id = tasks.add(b_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, b_task)
运行结果:
ids参数也可以被指定为一个函数,该函数id_func()将作用于params列表中的每一个元素。params是一个task对象列表,id_func()将调用单个task对象。
def id_func(fixture_value):
"""A function for generating ids."""
t = fixture_value
return 'Task({},{},{})'.format(t.summary, t.owner, t.done)
@pytest.fixture(params=tasks_to_try, ids=id_func)
def c_task(request):
"""Using a function (id_func) to generate ids."""
return request.param
def test_add_c(tasks_db, c_task):
"""Use fixture with generated ids."""
task_id = tasks.add(c_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, c_task)
运行结果:
注:对测试函数进行参数化处理,可以多次运行的只有测试函数,而使用参数化fixture,每个使用该fixture的测试函数都可以被运行多次,fixture的这一特性非常强大
三、问题锦集
问题1:执行时报错“ModuleNotFoundError: No module named ‘tasks’”
解决方案:使用import tasks或from tasks import something ,需要在本地使用pip 安装tasks包(源代码)。进入到tasks_proj根目录。运行以下命令进行安装操作
命令:
pip install . 或者pip install -e .