pytest测试框架使用笔记(一)

本文将会介绍Python最常用的测试框架之一的pytest,介绍该测试框架的常见用法,也是笔者近期使用该框架的一些总结。

pytest是一个功能强大的Python测试框架,广泛用于编写和运行单元测试、集成测试和功能测试。它与Python自带的unittest框架相似,但在使用上更为简洁和高效。

pytest的主要特点如下:

  • 易于上手:pytest具有直观的API,用户可以快速编写测试用例。
  • 支持多种测试类型:能够处理简单的单元测试到复杂的功能测试,包括Web自动化测试(如Selenium)和接口自动化测试(如requests)。
  • 参数化功能:允许通过参数化来简化多个用例的编写,使得同一测试函数可以使用不同的输入进行多次验证。
  • 丰富的插件生态:pytest支持多种第三方插件,如pytest-html(生成HTML格式的测试报告)、pytest-xdist(支持并行执行测试)等,用户还可以自定义扩展。
  • 灵活的跳过和失败处理:在测试执行过程中,可以选择跳过某些测试或标记预期失败的用例。
  • 与持续集成工具兼容:pytest能够方便地与Jenkins等持续集成工具进行集成。

pytest可以通过pip轻松安装。打开命令行并输入以下命令:

1
pip install -U pytest

安装完成后,可以通过以下命令检查版本以确认安装成功:

1
pytest --version

快速入门

一个简单的例子

以下是一个测试脚本的Python代码,文件名称为test_square.py。

1
2
3
4
5
6
7
8
9
10
11
12
# file name: test_square.py
import math


def test_sqrt():
num = 25
assert math.sqrt(num) == 5


def test_square():
num = 7
assert num ** 2 == 40

使用命令行测试:

1
pytest test_square.py

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
collected 2 items                                                                                                         

test_square.py .F [100%]

======================================================== FAILURES =========================================================
_______________________________________________________ test_square _______________________________________________________

def test_square():
num = 7
> assert num ** 2 == 40
E assert (7 ** 2) == 40

test_square.py:11: AssertionError
================================================= short test summary info =================================================
FAILED test_square.py::test_square - assert (7 ** 2) == 40
=============================================== 1 failed, 1 passed in 0.06s ===============================================

可以看到,上面测试命令运行了2个测试case,其中一个运行成功,用.标识,另一个运行失败,用F标识,并给出了报错信息。

如果需要输出详细信息,则在命令行中加入-v参数。

1
pytest -v test_square.py

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
collected 2 items                                                                                                         

test_square.py::test_sqrt PASSED [ 50%]
test_square.py::test_square FAILED [100%]

======================================================== FAILURES =========================================================
_______________________________________________________ test_square _______________________________________________________

def test_square():
num = 7
> assert num ** 2 == 40
E assert (7 ** 2) == 40

test_square.py:11: AssertionError
================================================= short test summary info =================================================
FAILED test_square.py::test_square - assert (7 ** 2) == 40
=============================================== 1 failed, 1 passed in 0.06s ===============================================

注意:测试文件名应以test_开头或以_test结尾。测试函数要以test_开头,不然不会加入测试中。测试类名应以Test开头,并且不能包含__init__方法。

如果将上述函数中的test_square函数名称改为a_test_square,则该函数将不会加入测试,使用pytest命令行运行(加-v参数)时,输出结果为:

1
2
3
4
5
collected 1 item                                                                                                          

test_square.py::test_sqrt PASSED [100%]

==================================================== 1 passed in 0.03s ====================================================

可以看到,函数名称为a_test_square的测试例子将不会加入测试,上述的测试只运行了一个测试例子,且测试成功。

注意: 如果在测试文件夹下只运行pytest -v,则pytest将会运行该目录下的所有测试case。

测试标记(Mark)

标记测试函数

默认情况下,pytest会执行文件中的所有以test_开头或结尾的测试函数。

如果我们需要运行特定的测试函数,则可以通过以下三种方式实现:

  • 显式指定函数名,通过::标记,比如命令pytest test_square.py::test_sqrt,则只会运行test_square.py中的test_sqrt函数
  • 使用模糊匹配,使用 -k 选项标识,比如命令pytest -k sqrt test_square.py
  • 使用pytest.mark在函数上进行标记。

使用pytest.mark时,对测试函数进行标识,如以下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import math
import pytest


@pytest.mark.finish
def test_sqrt():
num = 25
assert math.sqrt(num) == 5


@pytest.mark.progress
def test_square():
num = 7
assert num ** 2 == 40

测试时使用-m选择标记的测试函数,如命令:

1
pytest -v -m finish test_square.py

输出结果为:

1
2
3
collected 2 items / 1 deselected / 1 selected                                                                             

test_square.py::test_sqrt PASSED [100%]

注意:在使用mark功能时,同一个测试函数可以打多个标识,多个函数也可以打相同的标识。

跳过测试

使用pytest.mark.skip装饰器可跳过指定的测试函数。如以下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
import math
import pytest


def test_sqrt():
num = 25
assert math.sqrt(num) == 5


@pytest.mark.skip(reason="Not implemented yet")
def test_square():
num = 7
assert num ** 2 == 40

使用命令行pytest test_square.py,输出结果如下:

1
2
3
4
5
6
collected 2 items                                                                                                         

test_square.py::test_sqrt PASSED [ 50%]
test_square.py::test_square SKIPPED (unconditional skip) [100%]

============================================== 1 passed, 1 skipped in 0.03s ===============================================

可以看到,第二个测试函数被跳过了,不会加入此次测试。

pytest还支持使用 pytest.mark.skipif 为测试函数指定被忽略的条件。

标识预见错误

使用pytest.mark.xfail实现预见错误功能,即该测试函数的错误已经被预知了,因此测试结果为通过。如以下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
import math
import pytest


def test_sqrt():
num = 25
assert math.sqrt(num) == 5


@pytest.mark.xfail(reason="I know this will fail")
def test_square():
num = 7
assert num ** 2 == 40

使用命令pytest -v test_square.py进行测试,输出结果如下:

1
2
3
4
5
6
collected 2 items                                                                                                         

test_square.py::test_sqrt PASSED [ 50%]
test_square.py::test_square XFAIL (I know this will fail) [100%]

============================================== 1 passed, 1 xfailed in 0.04s ===============================================

可以看到test_square函数的测试结果为XFAIL,该测试也是通过的。

参数化测试

如果我们的测试函数需要对参数的多个值进行测试,一种方法是直接在测试函数中测试多个值的情形,但这只是一个测试case。较好的解决方法是参数化测试,即每组参数都独立执行一次测试。如以下的脚本:

1
2
3
4
5
6
import pytest


@pytest.mark.parametrize("num", [1, 2, 3, 4])
def test_square(num: int):
assert 0 < num ** 2 < 10

执行测试,输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
collected 4 items                                                                                                         

test_square.py::test_square[1] PASSED [ 25%]
test_square.py::test_square[2] PASSED [ 50%]
test_square.py::test_square[3] PASSED [ 75%]
test_square.py::test_square[4] FAILED [100%]

======================================================== FAILURES =========================================================
_____________________________________________________ test_square[4] ______________________________________________________

num = 4

@pytest.mark.parametrize("num", [1, 2, 3, 4])
def test_square(num: int):
> assert 0 < num ** 2 < 10
E assert (4 ** 2) < 10

test_square.py:6: AssertionError
================================================= short test summary info =================================================
FAILED test_square.py::test_square[4] - assert (4 ** 2) < 10
=============================================== 1 failed, 3 passed in 0.07s ===============================================

可以看到,上面的参数化测试执行了4个测试case,3个成功,1个失败,这使得参数化测试很有优势。

如果我们需要将测试组的默认参数显示更清晰,我们可以使用pytest.paramid参数进行自定义,修改如下:

1
2
3
4
5
6
7
8
9
10
import pytest


@pytest.mark.parametrize("num",
[pytest.param(1, id='case_1'),
pytest.param(2, id='case_2'),
pytest.param(3, id='case_3'),
pytest.param(4, id='case_4')])
def test_square(num: int):
assert 0 < num ** 2 < 10

如果需要将测试函数中的参数调整为多个参数,那么可以修改如下:

1
2
3
4
5
6
import pytest


@pytest.mark.parametrize("x, y", [(1, 2), (3, 4)])
def test_square(x: int, y: int):
assert 0 < x ** 2 + y ** 2 < 10

测试固件(Fixture)

固件(Fixture)是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们,类似于unittest框架 的相关测试固件,如setupteardown等。

pytest使用pytest.fixture() 来定义固件,更多时候,我们会使用conftest.py文件来管理固件。

调用方法

固件的调用方式有很多,这里介绍三种常见的调用方法:

  • 使用fixture函数名作为参数

如下面的脚本所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest


@pytest.fixture
def get_data():
data = {'name': 'John', 'age': 25}
return data


def test_user_name(get_data):
assert get_data['name'] == 'John'


def test_user_age(get_data):
assert get_data['age'] == 25

在上述例子中,test_user_name和test_user_age测试函数都是使用get_data固件。

  • 使用@pytest.mark.usefixtures('fixture函数名')装饰器

此时上面的脚本修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pytest

# 将data设置为全局变量
data = {}


@pytest.fixture
def get_data():
global data
data = {'name': 'John', 'age': 25}


@pytest.mark.usefixtures('get_data')
def test_user_name():
assert data['name'] == 'John'


@pytest.mark.usefixtures('get_data')
def test_user_age():
assert data['age'] == 25

需要注意的是,这里将data设置为全局变量,仅作为本次例子演示使用。

  • 使用autouse参数自动执行fixture函数

此时,上述的脚本修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pytest

# 将data设置为全局变量
data = {}


@pytest.fixture(autouse=True)
def get_data():
global data
data = {'name': 'John', 'age': 25}


def test_user_name():
assert data['name'] == 'John'


def test_user_age():
assert data['age'] == 25

当然, pytest也支持调用多个固件,和对固件进行参数化测试。以下是一个结合多个固件和参数化测试的例子:

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
import pytest

# 将data设置为全局变量
data = {}


@pytest.fixture(params=[{'name': 'John', 'age': 25},
{'name': 'Tom', 'age': 30}])
def get_data(request):
global data
data = request.param


@pytest.fixture()
def set_user_country():
global data
data['country'] = 'US'


@pytest.mark.usefixtures('get_data')
@pytest.mark.usefixtures('set_user_country')
def test_user_name_and_age():
assert data['name'] in ['John', 'Alan']
assert 0 < data['age'] < 100


@pytest.mark.usefixtures('get_data')
@pytest.mark.usefixtures('set_user_country')
def test_user_country():
assert data['country'] == 'US'

上面的例子中,共有2个固件:get_data, set_user_country,其中get_data设置data中的name和age,采用参数化测试,而set_user_country则设置data中的country.

执行上述测试,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
collected 4 items                                                                                                         

test_square.py::test_user_name_and_age[get_data0] PASSED [ 25%]
test_square.py::test_user_name_and_age[get_data1] FAILED [ 50%]
test_square.py::test_user_country[get_data0] PASSED [ 75%]
test_square.py::test_user_country[get_data1] PASSED [100%]

======================================================== FAILURES =========================================================
____________________________________________ test_user_name_and_age[get_data1] ____________________________________________

@pytest.mark.usefixtures('get_data')
@pytest.mark.usefixtures('set_user_country')
def test_user_name_and_age():
> assert data['name'] in ['John', 'Alan']
E AssertionError: assert 'Tom' in ['John', 'Alan']

test_square.py:23: AssertionError
================================================= short test summary info =================================================
FAILED test_square.py::test_user_name_and_age[get_data1] - AssertionError: assert 'Tom' in ['John', 'Alan']
=============================================== 1 failed, 3 passed in 0.06s ===============================================

与我们预期的一样,共有4个测试case,其中test_user_name_and_age当参数为{'name': 'Tom', 'age': 30}时会报错。

作用域

pytest的固件的作用域(scope)分为sessionmoduleclassfunction四个级别。在定义fixture函数的时候通过scope参数指定作用范围,默认为function。

  • function: 函数级,每个测试函数都会执行一次固件;
  • class: 类级别,每个测试类执行一次,所有方法都可以使用;
  • module: 模块级,每个模块执行一次,模块内函数和方法都可使用;
  • session: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。

关于固件的作用域,将在下面的配置文件conftest.py中进行演示。

配置文件conftest.py

在pytest框架中,conftest.py文件的主要用途是定义和共享测试固件(fixtures)及其他配置选项,以便在多个测试文件之间重用。这种方式可以减少代码重复,提高测试的可维护性和可读性。

conftest.py的用途如下:

  • 定义共享固件:

    固件是pytest中的一种功能,用于设置测试环境或者提供测试数据。通过在conftest.py中定义固件,可以使其在同一目录下的所有测试文件中可用。

  • 配置命令行选项:

    可以在conftest.py中添加自定义的命令行选项,以便在运行pytest时传递参数。

  • 添加钩子函数:

    可以使用钩子函数来扩展pytest的功能,例如修改测试收集过程或报告输出。

简单例子

以下是一个简单的示例,展示了如何在conftest.py中定义一个夹具,并在不同的测试文件中使用它:

  1. 创建 conftest.py
1
2
3
4
5
6
# conftest.py
import pytest

@pytest.fixture
def input_value():
return 42
  1. 创建测试文件 test_example1.py
1
2
3
4
5
6
7
8
# test_example1.py
import pytest

def test_divisible_by_2(input_value):
assert input_value % 2 == 0

def test_divisible_by_3(input_value):
assert input_value % 3 == 0
  1. 创建测试文件 test_example2.py
1
2
3
4
5
6
7
8
# test_example2.py
import pytest

def test_divisible_by_6(input_value):
assert input_value % 6 == 0

def test_divisible_by_7(input_value):
assert input_value % 7 == 0
  1. 运行测试

在终端中运行以下命令来执行这些测试:

1
pytest -v test_example1.py test_example2.py

输出示例如下所示:

1
2
3
4
test_example1.py::test_divisible_by_2 PASSED
test_example1.py::test_divisible_by_3 FAILED
test_example2.py::test_divisible_by_6 PASSED
test_example2.py::test_divisible_by_7 FAILED

通过这个示例,可以看到input_value夹具被成功地在多个测试文件中重用,从而避免了在每个测试文件中重复定义相同的数据或设置。这种方法使得测试代码更加整洁和易于管理。

作用域演示

conftest.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pytest


@pytest.fixture(scope="session")
def session_fixture():
print("这是一个作用于session的fixture")


@pytest.fixture(scope="module")
def module_fixture():
print("这是一个作用于module的fixture")


@pytest.fixture(scope="class")
def class_fixture():
print("这是一个作用于class的fixture")


@pytest.fixture(scope="function")
def function_fixture():
print("这是一个作用于function的fixture")

在test_example1.py测试文件中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# test_example1.py
import pytest


@pytest.mark.usefixtures("class_fixture")
@pytest.mark.usefixtures("function_fixture")
class TestOrder:

def test_a(self):
print("test_a")

def test_b(self):
print("test_b")


@pytest.mark.usefixtures("function_fixture")
def test_c():
print("test_c")

在文件中,TestOrder类使用了class_fixture, function_fixture固件,test_c只使用了function_fixture固件。

运行命令pytest -v -s test_example1.py后(-s支持终端输出),结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
collected 3 items                                                                                                         

test_example1.py::TestOrder::test_a 这是一个作用于class的fixture
这是一个作用于function的fixture
test_a
PASSED
test_example1.py::TestOrder::test_b 这是一个作用于function的fixture
test_b
PASSED
test_example1.py::test_c 这是一个作用于function的fixture
test_c
PASSED

==================================================== 3 passed in 0.02s ====================================================

TestOrder类中,class_fixture执行了一次,而test_a, test_b分别执行function_fixture一次。

异步测试

在pytest中进行异步测试,通常会用到 pytest-asyncio 插件,可直接使用pip方式安装。这个插件为pytest提供了对asyncio协程的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# @file: test_file.py
import pytest
import asyncio

# 被测异步函数
async def async_function(x):
await asyncio.sleep(1)
return x * 2

# 异步测试函数
@pytest.mark.asyncio
async def test_async_function():
result = await async_function(3)
assert result == 6

运行测试命令如下:

1
pytest -v test_file.py

总结

本文主要介绍了Python的主流测试框架之一的pytest,分别展示了pytest的基本用法,标识,固件,异步测试等使用方法,算是对pytest有了一个初步的了解。

后续文章将会介绍pytest的高级用法,敬请期待~


pytest测试框架使用笔记(一)
https://percent4.github.io/pytest测试框架使用笔记(一)/
作者
Jclian91
发布于
2025年1月8日
许可协议