Specifying Fixture Scope

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:

scope=’function’

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.

scope=’class’

Run once per test class, regardless of how many test methods are in the class.

scope=’module’

Run once per module, regardless of how many test functions or methods or other fixtures in the module use it.

scope=’session’

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.

Changing Scope for Tasks Project Fixtures

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.211.106