fixture是在测试函数运行之后由pytest执行的外壳函数。
以下是一个返回值的简单fixture
@pytest.fixture()定义一个fixture 用的时候在函数里直接传入名字
"""Demonstrate simple fixtures."""
import pytest
@pytest.fixture()
def some_data():
"""Return answer to ultimate question."""
return 42
def test_some_data(some_data):
"""Use fixture return value in a test."""
assert some_data == 42
@pytest.fixture()
def some_other_data():
"""Raise an exception from fixture."""
x = 43
assert x == 42
return x
def test_other_data(some_other_data):
"""Try to use failing fixture."""
assert some_data == 42
@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可以放到单独的文件里,如果你希望多个文件共享fixture,可以在某个公共的路径创建一个conftest.py,将fixture放在其中。
"""Define some fixtures to use in the project."""
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()
# Reminder of Task constructor interface
# Task(summary=None, owner=None, done=False, id=None)
# summary is required
# owner and done are optional
# id is set by database
@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)
可以按着目录级别放置conftest.py 在次路径下的所有案例讲执行。
fixture函数会在测试函数之前执行,但是如果fixture函数包含一个yield,那么系统将会在yield处停止,转而执行yield后面的代码。因此,可以将yield之前的代码视为配置(setup)过程,将yield之后的代码视为处理(teardown)过程。无论测试过程发生了什么,yield之后的代码都会被执行。
@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()
def test_add_returns_valid_id(tasks_db):
"""tasks.add() should return an integer."""
# GIVEN an initialized tasks db
# WHEN a new task is added
# THEN returned task_id is of type int
new_task = Task('do something')
task_id = tasks.add(new_task)
assert isinstance(task_id, int)
编写fixture时希望看到测试过程中执行的是什么,及执行的先后顺序,pytet提供了 --setup-show可以实现这个功能。
pytest --setup-show test_add.py -k valid_id
collected 3 items / 2 deselected / 1 selected
test_add.py
SETUP S tmp_path_factory
SETUP F tmp_path (fixtures used: tmp_path_factory)
SETUP F tmpdir (fixtures used: tmp_path)
SETUP F tasks_db (fixtures used: tmpdir)
func/test_add.py::test_add_returns_valid_id (fixtures used: request, tasks_db, tmp_path, tmp_path_factory, tmpdir).
TEARDOWN F tasks_db
TEARDOWN F tmpdir
TEARDOWN F tmp_path
TEARDOWN S tmp_path_factory
===================================================================== 1 passed, 2 deselected in 0.08s ======================================================================
Fixture 前面的S和F代表作用范围,F代表函数级别的 S代表会话级别的。3.6中会有总结。
fixture非常适合存放测试数据,并且它可以返回任何数据。
fixture失败案例会返回ERROR
fixture成功案例失败会返回Faile 这点pytest做的很好。
# 这是fixture没问题 案例断言会失败 案例状态执行后会为faile
@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本身有断言,在fixture中就失败了 案例是ERROR状态
@pytest.fixture()
def some_other_data():
"""Raise an exception from fixture."""
x = 43
assert x == 42
return x
def test_other_data(some_other_data):
"""Try to use failing fixture."""
assert some_data == 42
collected 3 items / 1 deselected / 2 selected
test_fixtures.py EF [100%]
================================================================================== ERRORS ==================================================================================
____________________________________________________________________ ERROR at setup of test_other_data _____________________________________________________________________@pytest.fixture() def some_other_data(): """Raise an exception from fixture.""" x = 43
assert x == 42 E assert 43 == 42
test_fixtures.py:21: AssertionError
================================================================================= FAILURES =================================================================================
_______________________________________________________________________________ test_a_tuple _______________________________________________________________________________a_tuple = (1, ‘foo’, None, {‘bar’: 23})
def test_a_tuple(a_tuple): """Demo the a_tuple fixture."""
assert a_tuple[3]['bar'] == 32 E assert 23 == 32
test_fixtures.py:38: AssertionError
我们可以用fixture返回多个数据
@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()
@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)
使用fixture的优势在:用户在编写测试函数时可以只考虑核心测试逻辑,而不是只考虑测试前的准备工作。
要习惯在注释中写 given 、 when、then,并且总是在fixture中尽量多的写given。这样做两个原因:
1.可读性高
2.fixture中的异常不会被报告为fail 而是error。测试失败不要原因是数据引起的,数据错误应该是发生了错误,而不是案例失败。这很重要。
def test_add_returns_valid_id(tasks_db):
"""tasks.add() should return an integer."""
# GIVEN an initialized tasks db
# WHEN a new task is added
# THEN returned task_id is of type int
new_task = Task('do something')
task_id = tasks.add(new_task)
assert isinstance(task_id, int)
fixture有一个scope的参数指定范围。
下面举个例子:
"""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."""
使用pytest --setup-show 命令可以看到具体调用
collected 4 items
test_scope.py
SETUP S sess_scope
SETUP M mod_scope
SETUP F func_scope
test_scope.py::test_1 (fixtures used: func_scope, mod_scope, sess_scope).
TEARDOWN F func_scope
SETUP F func_scope
test_scope.py::test_2 (fixtures used: func_scope, mod_scope, sess_scope).
TEARDOWN F func_scope
SETUP C class_scope
test_scope.py::TestSomething::test_3 (fixtures used: class_scope).
test_scope.py::TestSomething::test_4 (fixtures used: class_scope).
TEARDOWN C class_scope
TEARDOWN M mod_scope
TEARDOWN S sess_scope
==================================== 4 passed in 0.05s =======================================
不难看出作用范围是由fixture自身决定的,但还是要强调scope参数是在定义fixture时定义的,而不是在调用时,因此使用fixture的测试函数是无法改变fixture的作用范围的。
注意fixture只能使用同级别的fixture,或者比自己高级的fixture;比如 function函数级别的fixture可以使用同级别的fixture,亦可以使用class类级别、module模块级别、session会话级别;但是不可以反过来。
就是@pytest.mark.usefixtures(‘xxx’)的使用 很简单其余的没啥用其实,在类测试中用的话非常合适。
@pytest.fixture(scope='class')
def class_scope():
"""A class scope fixture."""
@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."""
为常用的fixture添加@autouse
"""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)
#运行测试 pytest -v -s test_autouse.py
#结果 都自动用到了两个测试用例。
collected 2 items
test_autouse.py::test_1 PASSED
test duration : 1.01 seconds
test_autouse.py::test_2 PASSED
test duration : 1.24 seconds
--
finished : 04 Apr 02:12:04
-----------------
=================================== 2 passed in 2.27s =====================================
fixture的名字展视通常会与他的函数名保持一致,但是pytest也运行重名名,name参数对fixture重命名。
"""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
collected 1 item
test_rename_fixture.py
SETUP F lue
test_rename_fixture.py::test_everything (fixtures used: lue).
TEARDOWN F lue
=============================== 1 passed in 0.02s ========================================
数据驱动fixture 可以让fixture运行多次 ,很强大的功能。
"""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)
@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)
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)