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

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

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.py 或 python -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,更灵活
参数化 subTest 或 parameterized @pytest.mark.parametrize
模拟 unittest.mock unittest.mock 或 mocker
跳过/预期失败 @unittest.skip / expectedFailure @pytest.mark.skip / xfail
插件生态 较少 极丰富 (xdist, cov, asyncio...)
学习曲线 较低,但代码冗长 稍高,但更简洁强大

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

相关推荐
weixin_440730502 分钟前
内置函数、json文本、pickle二进制
开发语言·python·json
殷色玫瑰9 分钟前
C++ string类详解:常用接口、字符串操作与模拟实现
java·linux·c语言·开发语言·数据结构·c++
李游Leo13 分钟前
HarmonyOS 7 + Spatial Recon Kit-C++:3DGS 高斯参数的非有限值隔离与可渲染性门禁【鸿蒙心迹】
开发语言·c++·3d·harmonyos
计算机毕业编程指导师37 分钟前
Python大数据毕设:基于Hadoop的病毒式社交媒体趋势和参与度分析系统怎么做 源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·计算机·毕业设计·课程设计·社交媒体
计算机毕业编程指导师39 分钟前
【计算机毕设选题推荐】基于Spark与Hadoop的AI就业收入区域差异分析系统 源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习 深度学习
大数据·python·数据分析·spark·毕业设计·课程设计·ai就业收入
小猴子爱上树43 分钟前
跨马翻译:批量图片翻译与视频字幕、智能抠图一站式在线图片翻译工具
大数据·人工智能·python·音视频
时间的拾荒人1 小时前
Qt 文件操作详解:从基础类到读写实战
开发语言·qt·面试
Wang's Blog1 小时前
Java 项目实战: 外卖平台优化-YApi接口管理平台与文档导入导出
java·开发语言·yapi
lzqrzpt1 小时前
临沂LED驱动电源工程选型标准与工艺对比解析
python·单片机·嵌入式硬件·物联网