pytest, as the most mainstream testing framework in the Python technology stack, is extremely powerful and flexible. Fixtures are at its core. Moreover, pytest also distinguishes different scopes for fixtures, which brings great flexibility to how we use them.
Let us take a look below at the role of each different scope.
Definition of fixture scope
First, according to the official documentation, the scope of a fixture in Pytest supports five settings: function (the default), classs, module, package, and session
| Scope | Description |
|---|
| function | Default value; takes effect for each test method (function), with a lifecycle at the test method level |
| class | Takes effect for the test class, with a lifecycle at the test class level |
| module | Takes effect for the test module, with a lifecycle at the module (file) level |
| package | Takes effect for the test package, with a lifecycle at the test package (directory) level |
| session | Takes effect for the test session, with a lifecycle at the session (one pytest run) level |
Let us illustrate this with code below. Suppose we currently have the following code structure

run_params is the method under test
1
2
3
4
5
6
7
8
9
10
| def deal_params(p):
print(f"input :{p}")
if type(p) is int:
return p*10
if type(p) is str:
return p*3
if type(p) in (tuple, list):
return "_".join(p)
else:
raise TypeError
|
test_ch_param and test_fixture_scope respectively define parameterization and the different test methods inside a test class
1
2
3
4
5
| import pytest
@pytest.mark.parametrize("param",[10, "城下秋草", "软件测试", ("示例", "代码")])
def test_params_mark(param):
print(deal_params(param))
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import pytest
class TestFixtureScope1:
def test_int(self):
assert deal_params(2) == 20
def test_str(self):
assert deal_params("秋草") == "秋草秋草秋草"
class TestFixtureScope2:
def test_list(self):
assert deal_params(["城下","秋草"]) == "城下_秋草"
def test_dict(self):
with pytest.raises(TypeError):
deal_params({"name": "秋草"})
|
In the common method file conftest.py, define the fixture prepare and set autouse=True, which means it will be applied automatically according to the configured scope of the fixture
1
2
3
4
5
| @pytest.fixture(autouse=True, scope='function')
def prepare():
print('-----some setup actions.....')
yield
print('-----some teardown actions!!')
|
Here we set the scope of prepare to different values respectively, and then obtain the corresponding outputs
function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| (.venv) C:\Chengxiaqiucao
pytest
======================================== test session starts =========================================
collected 8 items
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[10] -----some setup actions.....
input :10
100
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[城下秋草] -----some setup actions.....
input :城下秋草
城下秋草城下秋草城下秋草
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[软件测试] -----some setup actions.....
input :软件测试
软件测试软件测试软件测试
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[param3] -----some setup actions.....
input :('示例', '代码')
示例_代码
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_int -----some setup actions.....
input :2
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_str -----some setup actions.....
input :秋草
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_list -----some setup actions.....
input :['城下', '秋草']
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_dict -----some setup actions.....
input :{'name': '秋草'}
PASSED-----some teardown actions!!
========================================= 8 passed in 0.27s ==========================================
|
The fixture ran 8 times
class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| (.venv) C:\Chengxiaqiucao
pytest
======================================== test session starts =========================================
collected 8 items
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[10] -----some setup actions.....
input :10
100
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[城下秋草] -----some setup actions.....
input :城下秋草
城下秋草城下秋草城下秋草
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[软件测试] -----some setup actions.....
input :软件测试
软件测试软件测试软件测试
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[param3] -----some setup actions.....
input :('示例', '代码')
示例_代码
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_int -----some setup actions.....
input :2
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_str input :秋草
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_list -----some setup actions.....
input :['城下', '秋草']
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_dict input :{'name': '秋草'}
PASSED-----some teardown actions!!
========================================= 8 passed in 0.27s ==========================================
|
The test methods in test_ch_param, because they are defined directly in the file, also belong to the class level, so the fixture is also called each time a parameter is assigned. In test_fixture_scope, two test classes are explicitly defined, so it ran 2 times
module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| (.venv) C:\Chengxiaqiucao
pytest
======================================== test session starts =========================================
collected 8 items
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[10] -----some setup actions.....
input :10
100
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[城下秋草] input :城下秋草
城下秋草城下秋草城下秋草
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[软件测试] input :软件测试
软件测试软件测试软件测试
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[param3] input :('示例', '代码')
示例_代码
PASSED-----some teardown actions!!
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_int -----some setup actions.....
input :2
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_str input :秋草
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_list input :['城下', '秋草']
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_dict input :{'name': '秋草'}
PASSED-----some teardown actions!!
========================================= 8 passed in 0.27s ==========================================
|
After changing to the module scope, we can see that the fixture is called once for each module file
package
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| (.venv) C:\Chengxiaqiucao
pytest
======================================== test session starts =========================================
collected 8 items
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[10] -----some setup actions.....
input :10
100
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[城下秋草] input :城下秋草
城下秋草城下秋草城下秋草
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[软件测试] input :软件测试
软件测试软件测试软件测试
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[param3] input :('示例', '代码')
示例_代码
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_int input :2
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_str input :秋草
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_list input :['城下', '秋草']
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_dict input :{'name': '秋草'}
PASSED-----some teardown actions!!
========================================= 8 passed in 0.27s ==========================================
|
After changing to package, this is because the two test files are located in the same package, so it ran once
session
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| (.venv) C:\Chengxiaqiucao
pytest
======================================== test session starts =========================================
collected 8 items
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[10] -----some setup actions.....
input :10
100
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[城下秋草] input :城下秋草
城下秋草城下秋草城下秋草
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[软件测试] input :软件测试
软件测试软件测试软件测试
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[param3] input :('示例', '代码')
示例_代码
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_int input :2
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_str input :秋草
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_list input :['城下', '秋草']
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_dict input :{'name': '秋草'}
PASSED-----some teardown actions!!
========================================= 8 passed in 0.27s ==========================================
|
Finally, when set to session — that is, one execution session of running pytest — only then is a single fixture call triggered
So we can see that by defining different scopes for fixtures, we can determine the scope of the fixtures we write according to our needs, which offers great flexibility
Flexible Scope Definition for Complex Fixtures
Sometimes in actual use — especially when the initialization work of some of our fixtures is fairly complex but may be needed at different scopes at the same time — if we had to write several different fixtures merely because of the different scopes, the code would become rather redundant. How can we handle this? In fact, we can implement it flexibly using the contextmanager context.
For example, let us write another basic code context for a fixture:
1
2
3
4
5
| @contextmanager
def fixture_base():
print('~~~~~base fixture setup actions.....')
yield
print('~~~~~base fixture teardown actions!!')
|
Then for different fixtures, we can define different fixtures according to different scopes and call the context implementation here. For example, let us define another fixture with the scope set to package
1
2
3
4
5
6
7
| @pytest.fixture(autouse=False, scope='package')
def fixture_module():
"""
For complex fixtures where you want flexible scope handling, you can put the common code into a contextmanager,
and then define corresponding fixtures for different scopes
""" with fixture_base() as result:
yield result
|
👍👍This method comes from a summary by the pytest community, original issue link
Execution Order of Different Scopes
In the example above we actually saw the execution effect of package and session; because the test methods are all in the same package, there is no difference in effect. But in fact, different scopes also have an execution order
The order is summarized as follows:
session > package > module > class > function
After adding up to two fixtures here, the execution result is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| (.venv) C:\Chengxiaqiucao
pytest
======================================== test session starts =========================================
collected 8 items
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[10] -----some setup actions.....
~~~~~base fixture setup actions.....
input :10
100
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[城下秋草] input :城下秋草
城下秋草城下秋草城下秋草
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[软件测试] input :软件测试
软件测试软件测试软件测试
PASSED
BlogDemo/testDemo/test_ch_parm.py::test_params_mark[param3] input :('示例', '代码')
示例_代码
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_int input :2
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope1::test_str input :秋草
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_list input :['城下', '秋草']
PASSED
BlogDemo/testDemo/test_fixture_scope.py::TestFixtureScope2::test_dict input :{'name': '秋草'}
PASSED~~~~~base fixture teardown actions!!
-----some teardown actions!!
========================================= 8 passed in 0.27s ==========================================
|
We can see that session is executed earlier than package and is also torn down later.
So the above is the summary of pytest scope.
Let me also promote my testing course. Those who are interested can learn more through the link below~
❤️❤️❤️❤️ Chengxiaqiucao’s Test Career Advancement Course ❤️❤️❤️❤️