Python提高: unittest和 pytest的使用方法-由Deepseek产生

以下分别给出 unittestpytest 的详细案例,包含常见测试场景:基本断言、异常测试、夹具、模拟、参数化、跳过/预期失败。

1. 被测试代码 (待测模块 calculator.py)

python 复制代码
# calculator.py
import requests

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

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

def fetch_data(url):
    """模拟网络请求,返回状态码和JSON"""
    resp = requests.get(url)
    return resp.status_code, resp.json()

2. unittest 详细案例

python 复制代码
# test_unittest_demo.py
import unittest
from unittest.mock import patch, Mock
from calculator import Calculator, fetch_data

class TestCalculator(unittest.TestCase):
    """基本测试 + 夹具 + 模拟 + 参数化 + 跳过"""

    @classmethod
    def setUpClass(cls):
        """所有测试运行前执行一次"""
        print("\n[setUpClass] 初始化计算器实例")
        cls.calc = Calculator()

    @classmethod
    def tearDownClass(cls):
        """所有测试运行后执行一次"""
        print("[tearDownClass] 清理资源")

    def setUp(self):
        """每个测试方法前执行"""
        print(f"\n[setUp] 准备测试: {self._testMethodName}")

    def tearDown(self):
        """每个测试方法后执行"""
        print(f"[tearDown] 完成测试: {self._testMethodName}")

    # ---------- 基本断言 ----------
    def test_add_positive(self):
        self.assertEqual(self.calc.add(2, 3), 5)
        self.assertAlmostEqual(self.calc.add(0.1, 0.2), 0.3, places=6)

    def test_divide_normal(self):
        self.assertEqual(self.calc.divide(10, 2), 5.0)

    # ---------- 异常测试 ----------
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError) as ctx:
            self.calc.divide(5, 0)
        self.assertEqual(str(ctx.exception), "除数不能为零")

    # ---------- 模拟 (mock) ----------
    @patch('calculator.requests.get')
    def test_fetch_data_success(self, mock_get):
        # 配置模拟对象
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"key": "value"}
        mock_get.return_value = mock_response

        status, data = fetch_data("http://example.com")
        self.assertEqual(status, 200)
        self.assertEqual(data, {"key": "value"})
        mock_get.assert_called_once_with("http://example.com")

    @patch('calculator.requests.get')
    def test_fetch_data_failure(self, mock_get):
        mock_response = Mock()
        mock_response.status_code = 404
        mock_response.json.side_effect = Exception("Not Found")
        mock_get.return_value = mock_response

        with self.assertRaises(Exception):
            fetch_data("http://example.com/bad")

    # ---------- 参数化测试 (使用 subTest) ----------
    def test_add_multiple_inputs(self):
        test_cases = [(1, 2, 3), (-1, -1, -2), (0, 0, 0), (100, -50, 50)]
        for a, b, expected in test_cases:
            with self.subTest(a=a, b=b, expected=expected):
                self.assertEqual(self.calc.add(a, b), expected)

    # ---------- 跳过测试 ----------
    @unittest.skip("演示跳过,该功能尚未实现")
    def test_multiply_not_implemented(self):
        pass

    @unittest.skipIf(True, "条件满足时跳过")
    def test_skip_conditionally(self):
        pass

    @unittest.expectedFailure
    def test_expected_failure(self):
        # 这个断言会失败,但不会计入失败数
        self.assertEqual(1, 2)

if __name__ == '__main__':
    unittest.main(verbosity=2)

运行方式python test_unittest_demo.pypython -m unittest test_unittest_demo

3. pytest 详细案例

python 复制代码
# test_pytest_demo.py
import pytest
from unittest.mock import Mock, patch
from calculator import Calculator, fetch_data

# ---------- 夹具 (fixture) ----------
@pytest.fixture(scope="class")
def calculator():
    """类级别夹具,返回计算器实例"""
    print("\n[fixture] 创建计算器实例")
    calc = Calculator()
    yield calc
    print("[fixture] 销毁计算器实例")

@pytest.fixture
def sample_data():
    """函数级别夹具,返回测试数据"""
    return {"a": 10, "b": 5}

# ---------- 测试类 ----------
class TestCalculator:
    """使用类级别夹具"""

    def test_add(self, calculator, sample_data):
        result = calculator.add(sample_data["a"], sample_data["b"])
        assert result == 15  # pytest 使用原生 assert

    def test_divide_normal(self, calculator):
        assert calculator.divide(9, 3) == 3.0

    def test_divide_by_zero(self, calculator):
        with pytest.raises(ValueError, match="除数不能为零"):
            calculator.divide(5, 0)

# ---------- 参数化测试 (pytest 特色) ----------
@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (-1, -1, -2),
    (0, 0, 0),
    (100, -50, 50),
])
def test_add_parametrized(a, b, expected):
    calc = Calculator()
    assert calc.add(a, b) == expected

# 组合参数化 + 夹具
@pytest.mark.parametrize("input_val,expected", [(5, 5), (0, 0), (-3, -3)])
def test_identity(calculator, input_val, expected):
    assert calculator.add(input_val, 0) == expected

# ---------- 模拟 (使用 mocker fixture,需要安装 pytest-mock) ----------
# 也可以直接使用 unittest.mock,pytest 自动兼容
def test_fetch_data_success(mocker):
    # 使用 pytest-mock 的 mocker fixture
    mock_get = mocker.patch('calculator.requests.get')
    mock_response = Mock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"key": "value"}
    mock_get.return_value = mock_response

    status, data = fetch_data("http://example.com")
    assert status == 200
    assert data == {"key": "value"}
    mock_get.assert_called_once_with("http://example.com")

def test_fetch_data_failure():
    with patch('calculator.requests.get') as mock_get:
        mock_response = Mock()
        mock_response.status_code = 404
        mock_response.json.side_effect = Exception("Not Found")
        mock_get.return_value = mock_response

        with pytest.raises(Exception):
            fetch_data("http://example.com/bad")

# ---------- 跳过与预期失败 ----------
@pytest.mark.skip(reason="演示跳过,未实现功能")
def test_skip_example():
    pass

@pytest.mark.skipif(True, reason="条件满足时跳过")
def test_skip_conditionally():
    pass

@pytest.mark.xfail(reason="已知问题,暂时预期失败")
def test_expected_failure():
    assert 1 == 2

# ---------- 临时文件与 capsys (pytest 内置 fixture) ----------
def test_capsys_example(capsys):
    print("Hello, world!")
    captured = capsys.readouterr()
    assert captured.out == "Hello, world!\n"

def test_tmp_path(tmp_path):
    d = tmp_path / "sub"
    d.mkdir()
    f = d / "test.txt"
    f.write_text("pytest rocks")
    assert f.read_text() == "pytest rocks"

# ---------- 自定义标记 ----------
@pytest.mark.slow
def test_slow_operation():
    import time
    time.sleep(0.1)
    assert True

# 运行方式: pytest -m slow  (只运行标记为 slow 的测试)

运行方式

  • 安装 pytest:pip install pytest pytest-mock
  • 执行:pytest test_pytest_demo.py -v
  • 带覆盖率:pytest --cov=calculator test_pytest_demo.py

关键差异对比

特性 unittest pytest
断言风格 self.assertEqual(a, b) assert a == b
夹具 setUp / tearDown @pytest.fixture,更灵活
参数化 subTestparameterized @pytest.mark.parametrize
模拟 unittest.mock unittest.mockmocker
跳过/预期失败 @unittest.skip / expectedFailure @pytest.mark.skip / xfail
插件生态 较少 极丰富 (xdist, cov, asyncio...)
学习曲线 较低,但代码冗长 稍高,但更简洁强大

建议:新项目直接选择 pytest,旧项目维护可使用 unittest。两者可混合使用(pytest 能运行 unittest 风格的测试)。

相关推荐
pele2 小时前
Python Tkinter如何实现组件拖拽交换位置_计算鼠标坐标重排布局
jvm·数据库·python
Aiclin2 小时前
大模型基础-应用小记【转载】
python
2301_816660212 小时前
CSS实现盒子倒角不规则效果_利用border-radius多个值
jvm·数据库·python
Johnstons2 小时前
网络可观测性落地指南:从“出了问题才排查“到“实时感知全网状态“
开发语言·网络·php
2201_761040592 小时前
CSS如何根据父级容器宽度调整子项_利用容器查询container选择器css
jvm·数据库·python
️是782 小时前
信息奥赛一本通—编程启蒙(3371:【例64.2】 生日相同)
开发语言·c++·算法
ZPC82102 小时前
ROS2 快过UDP的方法
python·算法·机器人
weixin_458580122 小时前
如何在 Python Fabric 中正确执行 EdgeOS 配置命令
jvm·数据库·python
Kiling_07042 小时前
Java Math类核心用法全解析
java·开发语言