Fixtures include an optional parameter called scope, which controls how often a fixture gets set up and torn down. The scope parameter to @pytest.fixture() can have the values of function, class, module, or session. The default scope is function. The tasks_db fixture and all of the fixtures so far don’t specify a scope. Therefore, they are function scope fixtures.
Here’s a rundown of each scope value:
Run once per test function. The setup portion is run before each test using the fixture. The teardown portion is run after each test using the fixture. This is the default scope used when no scope parameter is specified.
Run once per test class, regardless of how many test methods are in the class.
Run once per module, regardless of how many test functions or methods or other fixtures in the module use it.
Run once per session. All test methods and functions using a fixture of session scope share one setup and teardown call.
Here’s how the scope values look in action:
| """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.""" |
Let’s use --setup-show to demonstrate that the number of times a fixture is called and when the setup and teardown are run depend on the scope:
| $ 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.01 seconds ====================== |
Now you get to see not just F and S for function and session, but also C and M for class and module.
Scope is defined with the fixture. I know this is obvious from the code, but it’s an important point to make sure you fully grok. The scope is set at the definition of a fixture, and not at the place where it’s called. The test functions that use a fixture don’t control how often a fixture is set up and torn down.
Fixtures can only depend on other fixtures of their same scope or wider. So a function scope fixture can depend on other function scope fixtures (the default, and used in the Tasks project so far). A function scope fixture can also depend on class, module, and session scope fixtures, but you can’t go in the reverse order.
With this knowledge of scope, let’s now change the scope of some of the Task project fixtures.
So far, we haven’t had a problem with test times. But it seems like a waste to set up a temporary directory and new connection to a database for every test. As long as we can ensure an empty database when needed, that should be sufficient.
To have something like tasks_db be session scope, you need to use tmpdir_factory, since tmpdir is function scope and tmpdir_factory is session scope. Luckily, this is just a one-line code change (well, two if you count tmpdir -> tmpdir_factory in the parameter list):
| 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 to depend on tasks_db_session, and we deleted all the entries to make sure it’s empty. Because we didn’t change its name, none of the fixtures or tests that already include it have to change.
The data fixtures just return a value, so there really is no reason to have them run all the time. Once per session is sufficient:
| # 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 of these changes 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 it’s all good. Let’s trace the fixtures for one test file to see if the different scoping worked as expected:
| $ pytest --setup-show tests/func/test_add.py |
| ======================== test session starts ======================== |
| 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) |
| tests/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) |
| tests/func/test_add.py::test_added_task_has_id_set |
| (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). |
| TEARDOWN F tasks_db |
| SETUP F tasks_db (fixtures used: tasks_db_session) |
| SETUP S tasks_just_a_few |
| SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) |
| tests/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_just_a_few |
| TEARDOWN S tasks_db_session |
| TEARDOWN S tmpdir_factory |
| |
| ===================== 3 passed in 0.03 seconds ====================== |
Yep. Looks right. tasks_db_session is called once per session, and the quicker tasks_db now just cleans out the database before each test.
3.144.222.185