📜 ⬆️ ⬇️

Python Testing with pytest. CHAPTER 3 pytest Fixtures

Back Next


This book is the missing chapter, missing from every comprehensive Python book.


Frank Ruiz
Principal Site Reliability Engineer, Box, Inc.



The examples in this book are written using Python 3.6 and pytest 3.2. pytest 3.2 supports Python 2.6, 2.7 and Python 3.3+


The source code for the Tasks project, as well as for all the tests shown in this book, is available via the link on the book's web page at pragprog.com . You do not need to download the source code to understand the test code; The test code is presented in a convenient form in the examples. But to follow along with the project objectives, or to adapt test examples to test your own project (your hands are untied!), You should go to the book’s web page and download the work. In the same place, on the book’s web page there is a link for errata messages and a discussion forum .

Under the spoiler is a list of articles in this series.



Now that you have seen the basics of pytest, let's turn our attention to fixtures, which are necessary for structuring test code for almost any non-trivial software system. Fixtures are functions performed by pytest before (and sometimes after) the actual test functions. Code in fixture can do everything you need. You can use fixtures to get a dataset for testing. You can use Fixtures to get the system in a known state before running the test. Fixtures are also used to retrieve data for multiple tests.


Here is a simple fixture example that returns a number:


ch3 / test_fixtures.py

 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 

The @pytest.fixture() decorator is used to tell pytest that a function is a fixture. When you include the name of the fixture in the parameter list of the test function, pytest knows how to start it before running the test. Fixtures can do work, and can return data to a test function.


The test test_some_data() has as a parameter the name of the some_data . pytest will detect this and find a fixture with that name. The name is significant in pytest. pytest will search for a fixture with that name in the test module. He will also search the conftest.py file if he does not find it in it.


Before we begin our study of fixtures (and the conftest.py file), I need to consider the fact that the term fixture has many meanings in the programming and testing community, and even in the Python community. I use the fixture , fixture function , and fixture method interchangeably to refer to the @pytest.fixture() functions described in this chapter. Fixture can also be used to refer to a resource that is referenced by a fixture function. Fixture functions often set up or extract some data that a test can work with. Sometimes these data are considered fixture. For example, the Django community often uses fixture to indicate some source data that is loaded into a database at the beginning of an application.


Regardless of other meanings, in pytest and in this book, test fixtures refer to the mechanism that pytest provides to separate the “preparing for (getting ready for)” and “cleaning up after” codes from your test functions.


pytest fixtures is one of the unique pieces that raise the pytest over other test environments, and are the reason why many respected people switch to ... and stay with pytest. However, the fixtures in pytest are different from those in Django and differ from the setup and teardown procedures found in unittest and nose. There are many features and nuances if we talk about fixtures. Once you get a good mental model of how they work, you will feel better. However, you need to play around with them for a while to enter, so let's get started.


Sharing Fixtures via conftest.py


You can place fixtures in separate test files, but to share fixtures in several test files, it is better to use the conftest.py file somewhere in a common place, centralized for all tests. For the task project, all fixtures will be in tasks_proj/tests/conftest.py .


From there, fixtures can be separated by any dough. You can put fixtures in separate test files if you want fixture to be used only in tests of this file. Similarly, you can have other conftest.py files in the subdirectories of the top tests directory. If you do this, the fixtures defined in these low-level conftest.py files will be available for tests in this directory and subdirectories. However, until now the fixtures in the “Tasks” project were intended for any test. Therefore, using all our tools in the conftest.py file in the root of the test, tasks_proj/tests , makes the most sense.


Although conftest.py is a Python module, it should not be imported by test files. Do not import the conftest or when! The conftest.py file is read by pytest and is considered a local plug-in, which will become clear when we start talking about plug-ins in chapter 5 “Plug-ins” on page 95. For now, tests/conftest.py as a place where we can put fixtures to use all tests in the test directory. Then let's rework some of our tests for task_proj to use fixtures correctly.


Using Fixtures for Setup and Teardown


Most of the tests in the Tasks project assume that the Tasks database is already configured, running, and ready. And we have to remove some records at the end if there is any need for cleaning. And you may also need to disconnect from the database. Fortunately, most of this is taken care of in the tasks code with tasks.start_tasks_db(<directory to store db\>, 'tiny' or 'mongo') and tasks.stop_tasks_db() ; we just need to call them at the right time, and we also need a temporary directory.


Fortunately, pytest includes an excellent fixture called tmpdir. We can use it for testing and do not have to worry about cleaning. This is not magic, just a good coding practice from the most inquisitive people. (Do not worry; we will analyze tmpdir and write it out in more detail using tmpdir_factory in the section “Using tmpdir and tmpdir_factory” on page 71.)


With all these components, this fixture works great:


ch3 / a / tasks_proj / tasks_proj /conftest.py

 import pytest import tasks from tasks import Task @pytest.fixture() def tasks_db(tmpdir): """    ,  .""" # Setup : start db tasks.start_tasks_db(str(tmpdir), 'tiny') yield #    # Teardown : stop db tasks.stop_tasks_db() 

The value of tmpdir is not a string — it is an object that represents a directory. However, it implements __str__ , so we can use str() to get the string to pass to start_tasks_db() . For now, we still use tiny for TinyDB.


The fixture function is run before the tests that use it. However, if the function has yield , then there will be a stop, the control will be passed to the tests and the line following the yield will be executed after the tests are completed. Therefore, think of the code above yield as “setup”, and of the code after yield as “teardown”. The code after the yield "teardown" will be executed regardless of what happens during the tests. We do not return data with the release in this fixture. But you can.


Let's change one of our tests to tasks.add() to use this fixture:


h3 / a / tasks_proj / tests / func / test_add .py

 import pytest import tasks from tasks import Task def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>)    .""" # GIVEN    # WHEN    # THEN  task_id  int new_task = Task('do something') task_id = tasks.add(new_task) assert isinstance(task_id, int) 

The main change here is that the additional fixture in the file has been removed, and we added tasks_db to the list of test parameters. I like to structure tests in the GIVEN / WHEN / THEN format (DANO / WHEN / AFTER) using comments, especially if it is not obvious from the code what is happening. I think this is useful in this case. I hope GIVEN initialized db tasks can help you figure out why tasks_db used as a tool for the test.




Make sure Tasks is installed.




We are still writing tests for the Tasks project in this chapter, which was first installed in chapter 2. If you missed this chapter, be sure to install the tasks with the cd code; pip install ./tasks_proj/ .




Tracing Fixture Execution with –setup-show


If you run the test from the last section, you will not see which fixtures are running:


 $ cd /path/to/code/ $ pip install ./tasks_proj/ #      $ cd /path/to/code/ch3/a/tasks_proj/tests/func $ pytest -v test_add.py -k valid_id ===================== test session starts ====================== collected 3 items test_add.py::test_add_returns_valid_id PASSED ====================== 2 tests deselected ====================== ============ 1 passed, 2 deselected in 0.02 seconds ============ 

When I develop fixtures, I need to see what works and when. Luckily, pytest provides this command line flag, -- setup-show , which does just that:


 $ pytest --setup-show test_add.py -k valid_id ============================= test session starts ============================= collected 3 items / 2 deselected test_add.py SETUP S tmpdir_factory SETUP F tmpdir (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tmpdir) func/test_add.py::test_add_returns_valid_id (fixtures used: tasks_db, tmpdir, tmpdir_factory). TEARDOWN F tasks_db TEARDOWN F tmpdir TEARDOWN S tmpdir_factory =================== 1 passed, 2 deselected in 0.18 seconds ==================== 

Our test is in the middle, and the pytest designated part of the SETUP and TEARDOWN for each fixture. Starting with test_add_returns_valid_id , you can see that tmpdir worked before the test. And before that tmpdir_factory . Apparently, tmpdir uses it as a fixture.


F and S in front of the fixture names indicate the area. F for scope and S for session area. I will talk about the scope in the “Scope of Fixture” section on page 56.


Using Fixtures for Test Data


Fixtures are a great place to store data for testing. You can return anything. Here is the fixture returning a mixed tuple:


ch3 / test_fixtures.py

 @pytest.fixture() def a_tuple(): """ -  """ return (1, 'foo', None, {'bar': 23}) def test_a_tuple(a_tuple): """Demo the a_tuple fixture.""" assert a_tuple[3]['bar'] == 32 

Since test_a_tuple() should fail (23! = 32) , we will see what happens when the fixture test fails:


 $ cd /path/to/code/ch3 $ pytest test_fixtures.py::test_a_tuple ============================= test session starts ============================= collected 1 item test_fixtures.py F [100%] ================================== 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 ========================== 1 failed in 0.17 seconds =========================== 

Together with the stack trace section, pytest displays the parameters of the function's value that caused the exception or did not pass assert. In the case of tests, fixtures are the parameters for the test, so they are reported using the stack trace. What happens if assert (or exception) happens in fixture?


 $ pytest -v test_fixtures.py::test_other_data ============================= test session starts ============================= test_fixtures.py::test_other_data ERROR [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 =========================== 1 error in 0.13 seconds =========================== 

A couple of things happen. The stack trace correctly indicates that assert occurred in a fixture function. In addition, test_other_data is not reported as FAIL , but as ERROR . This is a big difference. If the test suddenly fails, you know that the failure occurred in the test itself, and does not depend on some kind of fixture.


But what about the Tasks project? For the Tasks project, we could probably use some data fixtures, perhaps different task lists with different properties:


ch3 / a / tasks_proj / tests / conftest.py

 #    Task constructor # Task(summary=None, owner=None, done=False, id=None) # summary    # owner  done   # id    @pytest.fixture() def tasks_just_a_few(): """    .""" 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(): """     .""" 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')) 

You can use them directly from tests, or from other fixtures. Let's create with them non-empty databases for testing.


Using Multiple Fixtures


You have already seen that tmpdir uses tmpdir_factory. And you used tmpdir in our task_db fixture. Let's continue the chain and add some specialized fixtures for the non-empty tasks project database:


ch3 / a / tasks_proj / tests / conftest.py

 @pytest.fixture() def db_with_3_tasks(tasks_db, tasks_just_a_few): """   3 ,  .""" for t in tasks_just_a_few: tasks.add(t) @pytest.fixture() def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): """   9 , 3 owners,  3   .""" for t in tasks_mult_per_owner: tasks.add(t) 

All these fixtures include two fixtures in their parameter list: tasks_db and a data set. The data set is used to add tasks to the database. Now tests can use them if you want the test to start with a non-empty database, for example:


ch3 / a / tasks_proj / tests / func / test_add.py

 def test_add_increases_count(db_with_3_tasks): """Test tasks.add()    tasks.count().""" # GIVEN db  3  # WHEN     tasks.add(Task('throw a party')) # THEN    1 assert tasks.count() == 4 

It also demonstrates one of the main reasons for using fixtures: to focus the test on what you are actually testing, and not on what you had to do to prepare for the test. I like to use comments for GIVEN / WHEN / THEN and try to push as much data (GIVEN) as possible into fixtures for two reasons. First, it makes the test more readable and, therefore, more maintainable. Secondly, an assert or exception in a fixture results in an error (ERROR), while an assert or exception in a test function causes an error (FAIL). I do not want test_add_increases_count() to test_add_increases_count() if the database initialization failed. It is just confusing. I want the failure (FAIL) test_add_increases_count() to be possible only if add () really could not change the counter. Let's run and see how all fixtures work:


 $ cd /path/to/code/ch3/a/tasks_proj/tests/func $ pytest --setup-show test_add.py::test_add_increases_count ============================= test session starts ============================= collected 1 item test_add.py SETUP S tmpdir_factory SETUP F tmpdir (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tmpdir) SETUP F tasks_just_a_few SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) func/test_add.py::test_add_increases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_just_a_few, tmpdir, tmpdir_factory). TEARDOWN F db_with_3_tasks TEARDOWN F tasks_just_a_few TEARDOWN F tasks_db TEARDOWN F tmpdir TEARDOWN S tmpdir_factory ========================== 1 passed in 0.20 seconds =========================== 

We received again a bunch of Fs and S for the function and scope of the session. Let's see what it is.


Area Specification (Scope) Fixture


Fixtures include an optional parameter called scope , which determines how often the fixture gets setup and torndown. The scope parameter for @ pytest.fixture() can be a function, class, module, or session. The default scope is a function. The tasks_db settings and all fixtures do not yet define an area. Thus, they are functional fixtures.


The following is a brief description of each Scope value:



Here is what the scope values ​​look like in action:


ch3 / test_scope.py

 """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): """   ,   .""" def test_2(sess_scope, mod_scope, func_scope): """     .""" @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.""" 

Let's use --setup-show to demonstrate that the number of fixture and setup calls paired with teardown are performed depending on the area:


 $ cd /path/to/code/ch3/ $ pytest --setup-show test_scope.py ============================= test session starts ============================= 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.11 seconds =========================== 

Now you can see not only F and S for the function and session, but also C and M for the class and module.


The scope is determined using the fixture. I know that this is obvious from the code, but this is an important point to make sure that you are completely grokate. ( Translator’s note : groaning - most likely the author means a term from Robert Heinlein’s novel The Stranger in a Foreign Country. Approximate meaning) deeply and intuitively understand"). The scope is specified in the fixture definition, and not in the place where it was called. Test functions that use fixture do not control how often SETUP and fixture breaks (TEARDOWN).


Fixtures can depend only on other fixtures from the same or more extended scope. Thus, function scope fixture may depend on other function scope fixture (by default, it is still used in the Tasks project). The function scope fixture may also depend on the class, module and fixtures of the session area, but never in the reverse order.


Change Scope for Tasks Project Fixtures


Given this knowledge of scope, let's now change the scope of some of the Task project fixtures.


Until now, we have not had problems with testing time. But, you see, it is useless to create a temporary directory and a new database connection for each test. As long as we can provide an empty database when necessary, this should be enough.


To use something like tasks_db as a session area, you need to use tmpdir_factory , since tmpdir is the scope of the function and tmpdir_factory is the scope of the session. Fortunately, this is just one line of code change (well, two, if you tmpdir->tmpdir_factory in the parameter list):


ch3 / b / tasks_proj / tests / conftest.py

 """Define some fixtures to use in the project.""" import pytest import tasks from tasks import Task @pytest.fixture(scope='session') def tasks_db_session(tmpdir_factory): """Connect to db before tests, disconnect after.""" temp_dir = tmpdir_factory.mktemp('temp') tasks.start_tasks_db(str(temp_dir), 'tiny') yield tasks.stop_tasks_db() @pytest.fixture() def tasks_db(tasks_db_session): """An empty tasks db.""" tasks.delete_all() 

Here we changed tasks_db depending on tasks_db_session , and we deleted all the entries to make sure that it is empty. Since we have not changed its name, none of the fixtures or tests that already include it should be changed.


Data fixtures simply return value, so there’s really no reason for them to work all the time. Once per session is enough:


ch3 / b / tasks_proj / tests / conftest.py

 # 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(scope='session') 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(scope='session') 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')) 

Now let's see if all these changes will work with our tests:


 $ cd /path/to/code/ch3/b/tasks_proj $ pytest ===================== test session starts ====================== collected 55 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ....... tests/func/test_unique_id.py . tests/unit/test_task.py .... ================== 55 passed in 0.17 seconds =================== 

Looks like everything's fine. Let's follow the fixtures for a single test file to see that different areas work according to our expectations:


 $ pytest --setup-show tests/func/test_add.py ============================= test session starts ============================= platform win32 -- Python 3.6.5, pytest-3.9.3, py-1.7.0, pluggy-0.8.0 rootdir: c:\_BOOKS_\pytest_si\bopytest-code\code\ch3\b\tasks_proj\tests, inifile: pytest.ini collected 3 items tests\func\test_add.py SETUP S tmpdir_factory SETUP S tasks_db_session (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tasks_db_session) func/test_add.py::test_add_returns_valid_id (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). TEARDOWN F tasks_db SETUP F tasks_db (fixtures used: tasks_db_session) func/test_add.py::test_added_task_has_id_set (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). TEARDOWN F tasks_db SETUP S tasks_just_a_few SETUP F tasks_db (fixtures used: tasks_db_session) SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) func/test_add.py::test_add_increases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_db_session, tasks_just_a_few, tmpdir_factory). TEARDOWN F db_with_3_tasks TEARDOWN F tasks_db TEARDOWN S tasks_db_session TEARDOWN S tmpdir_factory TEARDOWN S tasks_just_a_few ========================== 3 passed in 0.24 seconds =========================== 

Yeah. . tasks_db_session , task_db .


Specifying Fixtures with usefixtures


, , , . , @pytest.mark.usefixtures('fixture1', 'fixture2') . usefixtures , , . — . :


ch3/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 , . , , . , - usefixtures , .


autouse Fixtures That Always Get Used ( )


, , ( usefixtures ). autouse=True , . , , . :


ch3/test_autouse.py

 """ autouse fixtures.""" import pytest import time @pytest.fixture(autouse=True, scope='session') def footer_session_scope(): """    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(): """     .""" start = time.time() yield stop = time.time() delta = stop - start print('\ntest duration : {:0.3} seconds'.format(delta)) def test_1(): """   .""" time.sleep(1) def test_2(): """    .""" time.sleep(1.23) 

, . :


 $ cd /path/to/code/ch3 $ pytest -v -s test_autouse.py ===================== test session starts ====================== collected 2 items test_autouse.py::test_1 PASSED test duration : 1.0 seconds test_autouse.py::test_2 PASSED test duration : 1.24 seconds -- finished : 25 Jul 16:18:27 ----------------- =================== 2 passed in 2.25 seconds =================== 

autouse . But this is the exception rather than the rule. , .


, autouse , , tasks_db . Tasks , , , API . . , .


Fixtures


, , , . , pytest name @pytest.fixture() :


ch3/ test_rename_fixture.py

 """ fixture renaming.""" import pytest @pytest.fixture(name='lue') def ultimate_answer_to_life_the_universe_and_everything(): """  .""" return 42 def test_everything(lue): """   .""" assert lue == 42 

lue fixture , fixture_with_a_name_much_longer_than_lue . , --setup-show :


 $ pytest --setup-show test_rename_fixture.py ======================== test session starts ======================== collected 1 items test_rename_fixture.py SETUP F lue test_rename_fixture.py::test_everything (fixtures used: lue). TEARDOWN F lue ===================== 1 passed in 0.01 seconds ====================== 

, lue , pytest --fixtures . , , , :


 $ pytest --fixtures test_rename_fixture.py ======================== test session starts ======================= ... ------------------ fixtures defined from test_rename_fixture ------------------ lue Return ultimate answer. ================= no tests ran in 0.01 seconds ================= 

— . , , , , , . , lue . «Tasks»:


 $ cd /path/to/code/ch3/b/tasks_proj $ pytest --fixtures tests/func/test_add.py ======================== test session starts ======================== ... tmpdir_factory Return a TempdirFactory instance for the test session. tmpdir Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. ----------------------- fixtures defined from conftest ------------------------ tasks_db An empty tasks db. tasks_just_a_few All summaries and owners are unique. tasks_mult_per_owner Several owners with several tasks each. db_with_3_tasks Connected db with 3 tasks, all unique. db_with_multi_per_owner Connected db with 9 tasks, 3 owners, all with 3 tasks. tasks_db_session Connect to db before tests, disconnect after. =================== no tests ran in 0.01 seconds ==================== 

Cool! conftest.py . tmpdir tmpdir_factory , .



[Parametrized Testing] , . 42, . . - , , :


ch3/b/tasks_proj/tests/func/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))

, , a_task :


ch3/b/tasks_proj/tests/func/ test_add_variety2.py

@pytest.fixture(params=tasks_to_try)
def a_task(request):
""" ."""
return request.param

def test_add_a(tasks_db, a_task):
""" a_task ( ids)."""
task_id = tasks.add(a_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, a_task)

, fixture, , . . param, , params @pytest.fixture(params=tasks_to_try) .


a_task — request.param , . , , :


 $ cd /path/to/code/ch3/b/tasks_proj/tests/func $ pytest -v test_add_variety2.py::test_add_a ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_a[a_task0] PASSED test_add_variety2.py::test_add_a[a_task1] PASSED test_add_variety2.py::test_add_a[a_task2] PASSED test_add_variety2.py::test_add_a[a_task3] PASSED =================== 4 passed in 0.03 seconds =================== 

, pytest , () . , :


ch3/b/tasks_proj/tests/func/ test_add_variety2.py

@pytest.fixture(params=tasks_to_try, ids=task_ids)
def b_task(request):
""" ."""
return request.param

def test_add_b(tasks_db, b_task):
""" b_task, ."""
task_id = tasks.add(b_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, b_task)

:


 $ pytest -v test_add_variety2.py::test_add_b ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_b[Task(sleep,None,True)] PASSED test_add_variety2.py::test_add_b[Task(wake,brian,False)] PASSED test_add_variety2.py::test_add_b[Task(breathe,BRIAN,True)] PASSED test_add_variety2.py::test_add_b[Task(exercise,BrIaN,False)] PASSED =================== 4 passed in 0.04 seconds =================== 

ids , , . , :


ch3/b/tasks_proj/tests/func/ test_add_variety2.py

def id_func(fixture_value):
""" ."""
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):
""" (id_func) ."""
return request.param

def test_add_c(tasks_db, c_task):
""" ."""
task_id = tasks.add(c_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, c_task)

. Task, id_func() Task , namedtuple Task Task . , , :


 $ pytest -v test_add_variety2.py::test_add_c ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_c[Task(sleep,None,True)] PASSED test_add_variety2.py::test_add_c[Task(wake,brian,False)] PASSED test_add_variety2.py::test_add_c[Task(breathe,BRIAN,True)] PASSED test_add_variety2.py::test_add_c[Task(exercise,BrIaN,False)] PASSED =================== 4 passed in 0.04 seconds =================== 

. , , . , !


Fixtures Tasks Project


, Tasks. TinyDB . , . , , , , TinyDB , MongoDB .


( ), , start_tasks_db() tasks_db_session :


ch3/b/tasks_proj/tests/conftest.py

""" ."""

import pytest
import tasks
from tasks import Task

@pytest.fixture(scope='session')
def tasks_db_session(tmpdir_factory):
""" , ."""
temp_dir = tmpdir_factory.mktemp('temp')
tasks.start_tasks_db(str(temp_dir), 'tiny')
yield
tasks.stop_tasks_db()

@pytest.fixture()
def tasks_db(tasks_db_session):
""" tasks."""
tasks.delete_all()

db_type start_tasks_db() . , :


tasks_proj/src/tasks/api.py

  def start_tasks_db(db_path, db_type): # type: (str, str) -None """  API  .""" if not isinstance(db_path, string_types): raise TypeError('db_path must be a string') global _tasksdb if db_type == 'tiny': import tasks.tasksdb_tinydb _tasksdb = tasks.tasksdb_tinydb.start_tasks_db(db_path) elif db_type == 'mongo': import tasks.tasksdb_pymongo _tasksdb = tasks.tasksdb_pymongo.start_tasks_db(db_path) else: raise ValueError("db_type   'tiny'  'mongo'") 

MongoDB, db_type mongo. :


ch3/c/tasks_proj/tests/conftest.py

  import pytest import tasks from tasks import Task # @pytest.fixture(scope='session', params=['tiny',]) @pytest.fixture(scope='session', params=['tiny', 'mongo']) def tasks_db_session(tmpdir_factory, request): """Connect to db before tests, disconnect after.""" temp_dir = tmpdir_factory.mktemp('temp') tasks.start_tasks_db(str(temp_dir), request.param) yield # this is where the testing happens tasks.stop_tasks_db() @pytest.fixture() def tasks_db(tasks_db_session): """An empty tasks db.""" tasks.delete_all() 

params=['tiny',' mongo'] -. request temp_db db_type request.param , "tiny" "mongo".


--verbose -v pytest , pytest . , .




Installing MongoDB




MongoDB, , MongoDB pymongo . MongoDB, https://www.mongodb.com/download-center . pymongo pip— pip install pymongo . MongoDB ; 7 .




:


  $ cd /path/to/code/ch3/c/tasks_proj $ pip install pymongo $ pytest -v --tb=no ===================== test session starts ====================== collected 92 items test_add.py::test_add_returns_valid_id[tiny] PASSED test_add.py::test_added_task_has_id_set[tiny] PASSED test_add.py::test_add_increases_count[tiny] PASSED test_add_variety.py::test_add_1[tiny] PASSED test_add_variety.py::test_add_2[tiny-task0] PASSED test_add_variety.py::test_add_2[tiny-task1] PASSED ... test_add.py::test_add_returns_valid_id[mongo] FAILED test_add.py::test_added_task_has_id_set[mongo] FAILED test_add.py::test_add_increases_count[mongo] PASSED test_add_variety.py::test_add_1[mongo] FAILED test_add_variety.py::test_add_2[mongo-task0] FAILED ... ============= 42 failed, 50 passed in 4.94 seconds ============= 

Hm Bummer. , , - Mongo. , pdb: , . 125. TinyDB.


Exercises


  1. test_fixtures.py .
    2. fixtures—functions @pytest.fixture() , . , , .
  2. , .
  3. , .
  4. pytest --setup-show test_fixtures.py . ?
  5. scope= 'module' 4.
  6. pytest --setup-show test_fixtures.py . ?
  7. 6 return <data> yield <data> .
  8. yield .
  9. pytest -s -v test_fixtures.py . ?

What's next


pytest fixture , , building blocks , setup teardown , (, Mongo TinyDB). , , .


pytest, , (builtin) tmpdir tmpdir_factory. (builtin) .


Back Next

Source: https://habr.com/ru/post/448786/


All Articles