pytest 入门使用手册

pytest 是一个功能强大且灵活的 Python 测试框架,身为一个开发者学好怎么写测试是基本的必要条件,以下是我对 pytest 的使用心得。

开发环境

  • Windows 11 Home
  • PyCharm 2024.3.5
  • Python 3.13.2
  • uv 0.6.10
  • pytest 8.3.5
  • pytest-cov 6.1.0
  • pytest-html>=4.1.1
     

安装环境

用 uv 建立 python 专案

uv init Lab.Py.TestProject
cd Lab.Py.TestProject

 

在 python 专案安装测试套件

uv add pytest

 

测试起手式

基本原则

建立测试档案

  • 测试类别名称需以 Test 开头。
  • 测试函数名称需以 test 开头。

 

被测目标物

# calculator.py
class Calculator:
    """
    一个简单的计算器类,提供基本的数学运算功能。
    提供的运算包括加法、减法、乘法和除法。
    使用範例:
    calc = Calculator()
    result = calc.add(5, 3)
    result = calc.subtract(5, 3)
    result = calc.multiply(5, 3)
    result = calc.divide(5, 3)

    """

    def add(self,a, b):
        return a + b

    def subtract(self,a, b):
        return a - b

    def multiply(self,a, b):
        return a * b

    def divide(self,a, b):
        if b == 0:
            raise ValueError("除数不能为零")
        return a / b

 

撰写测试

範例程式码:

# calculator_test.py
import pytest
from calculator import Calculator

class TestCalculator:
    def test_add(self):
        target = Calculator()
        assert target.add(2, 3) == 5

    def test_subtract(self):
        target = Calculator()
        assert target.subtract(5, 3) == 2

    def test_multiply(self):
        target = Calculator()
        assert target.multiply(4, 3) == 12

    def test_divide(self):
        target = Calculator()
        assert target.divide(10, 2) == 5

 

验证/断言

assert

结果跟期望值比对

assert target.divide(10, 2) == 5

 

pytest.raises

检测是否抛出预期的例外。

    def test_divide_by_zero(self):
        target = Calculator()
        with pytest.raises(ValueError):
            target.divide(10, 0)

 

执行测试

测试多个档案

如果专案包含多个测试档案,在终端机中执行以下指令:

uv run pytest

 

指定特定档案

uv run pytest test_example.py

 

显示详细测试结果

uv run pytest -v

 

仅执行失败的测试

uv run pytest --lf

 

产生 HTML 测试报告

在 python 专案安装测试报告套件

uv add pytest-html

 

执行测试并输出报告:

uv run pytest --html=report.html

 

report.html 效果如下

 

产生 Allure 测试报告

在作业系统安装 allure

scoop install allure

 

在 python 专案安装 allure-pytest 套件

uv add allure-pytes

 

执行测试并产生 allure result

uv run pytest --alluredir=allure-results

 

挂起 allure server

allure serve allure-results

 

执行结果如下:

 

测试涵盖率

uv run pytest --cov=calculator

 

参数化测试

使用 装饰子 decorator  @pytest.mark.parametrize 定义传入参数值、验证值,就可以批次执行测试,类比 .NET 的测试框架 MsTest、XUnit、NUnit

# test_parametrize.py
import pytest
from calculator import Calculator


@pytest.mark.parametrize("first, second, expected", [
    (2, 3, 5),
    (-1, 1, 0),
    (0, 0, 0),
])
def test_add(first, second, expected):
    calculator = Calculator()
    assert calculator.add(first, second) == expected

 

执行结果如下

 

fixture/scope

fixture 是用来定义测试,需要共用资源的修饰子,共用的範围 scope 有以下层级:

  • function
  • class
  • module
  • session
     
  • 预设的 scope="function"
  • 测试方法依赖 @fixture 方法,例如,setup_and_cleanup_function

每一个测试 function 执行前后设置及清理

当 scope="function",会在每一个 function 分别执行一次设置和清理

# test_fixtures.py
import pytest
from calculator import Calculator


@pytest.fixture(scope="function")
def setup_and_cleanup_function():
    print("\n")
    print("每一个 function 个别执行一次设定")
    yield
    print("\n")
    print("每一个 function 个别执行一次清理")


def test_add_1(setup_and_cleanup_function):
    target = Calculator()
    assert target.add(2, 3) == 5


class TestCalculator:
    def test_add(self, setup_and_cleanup_function):
        target = Calculator()
        assert target.add(2, 3) == 5

    ... 省略

 

每一个测试类别 (class) 执行前后设置及清理

当 scope="class",会在第一个测试方法前执行一次设置,最后一个方法执行后执行一次清理

# test_fixtures_class.py
import pytest
from calculator import Calculator


@pytest.fixture(scope="class")
def setup_and_cleanup_module():
    print("\n")
    print("每一个 class 执行一次设置\n")
    yield
    print("\n")
    print("每一个 class 执行一次清理\n")


def test_add_1(setup_and_cleanup_module):
    target = Calculator()
    assert target.add(2, 3) == 5


class TestCalculator:
    def test_add_2(self, setup_and_cleanup_module):
        target = Calculator()
        assert target.add(2, 3) == 5

 

当前 .py 档的 class 和 function 只执行一次设置和清理

当 scope="module",当前 .py 档,class 和 function 只执行一次设置和清理

# test_fixtures_module.py
import pytest
from calculator import Calculator


@pytest.fixture(scope="module")
def setup_and_cleanup_module():
    print("\n")
    print("当前 .py 档,class 和 function 只执行一次设置\n")
    yield
    print("\n")
    print("当前 .py 档,class 和 function 只执行一次清理\n")


def test_add_1(setup_and_cleanup_module):
    target = Calculator()
    assert target.add(2, 3) == 5


class TestCalculator:
    def test_add_2(self, setup_and_cleanup_module):
        target = Calculator()
        assert target.add(2, 3) == 5

    ... 省略

 

每一个测试 Session (package) 执行一次设置及清理

scope="session" 是在当前这个测试 Session (package) 的前后进行设置 / 清理工作,可以包含多个 .py 档

# test_fixtures_session.py
import pytest
from calculator import Calculator


@pytest.fixture(scope="session")
def setup_and_cleanup_session():
    print("\n")
    print("每一个测试 session 只执行一次设置\n")
    yield
    print("\n")
    print("每一个测试 session 只执行一次清理\n")


def test_add_1(setup_and_cleanup_session):
    target = Calculator()
    assert target.add(2, 3) == 5


class TestCalculator:
    def test_add_2(self, setup_and_cleanup_session):
        target = Calculator()
        assert target.add(2, 3) == 5
	... 省略

 

按下 F5  或是  Alt+F5 观察,设置和清理的生命週期

 

或是用以下脚本观察

uv run pytest -v -s test_fixtures_module.py

 

分类测试方法

当测试方法越来越多的时候,分类它们有助于阅读跟查找、执行

当使用 @pytest.mark.demo 标记分类,demo 代表分类名称

# test_fixtures_mark.py
import pytest
from calculator import Calculator


@pytest.mark.demo
class TestCalculator:
    def test_add(self):
        target = Calculator()
        assert target.add(2, 3) == 5

    def test_subtract(self):
        target = Calculator()
        assert target.subtract(5, 3) == 2

    def test_multiply(self):
        target = Calculator()
        assert target.multiply(4, 3) == 12

    def test_divide(self):
        target = Calculator()
        assert target.divide(10, 2) == 5

    @pytest.mark.skip
    def test_divide_by_zero(self):
        target = Calculator()
        with pytest.raises(ValueError):
            target.divide(10, 0)

 

调用测试时,指定要执行哪一个分类,传入 -m demo,这样一来就只会执行 demo 类别的测试

uv run pytest -v -s -m demo

 

执行结果如下:

 

跳过测试方法

装饰子 pytest.mark.skip,可以用来标记不执行,当执行测试时,会略过它

@pytest.mark.skip(reason="搞不定,先跳过")
def test_divide_by_zero(self):
    target = Calculator()
    with pytest.raises(ValueError):
        target.divide(10, 0)

 

执行结果如下:

 

範例位置

https://github.com/yaochangyu/sample.dotblog/tree/master/Test/Lab.Py.TestProject

 

参考

https://docs.pytest.org/en/stable/getting-started.html

若有谬误,烦请告知,新手发帖请多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章