🔎大家好,我是ZTLJQ,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流

📝个人主页-ZTLJQ的主页

🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​📣系列果你对这个系列感兴趣的话

专栏 - ​​​​​​Python从零到企业级应用:短时间成为市场抢手的程序员

✔说明⇢本人讲解主要包括Python爬虫、JS逆向、Python的企业级应用

如果你对这个系列感兴趣的话,可以关注订阅哟👋

为代码上一道保险

软件开发过程中,Bug无处不在。随着代码库的增大,一个看似微小的改动可能会引发意想不到的连锁反应,导致原本正常的功能失效。单元测试(Unit Testing)就像是为你的代码购买的一份“保险”,它通过自动化的方式,对代码的最小可测试单元(通常是函数或方法)进行验证,确保它们在各种情况下都能按预期工作。

单元测试不仅能提高代码质量,还能在重构时提供信心,让你大胆地改进代码而不用担心破坏现有功能。本篇博客将带你深入Python的单元测试世界,从基础概念到高级技巧,助你写出更可靠、更易维护的代码。


第一部分:核心概念
  • 单元 (Unit): 指的是代码中最小的可测试部分,通常是一个函数、一个方法或一个类。
  • 单元测试 (Unit Test): 是一段自动化代码,用于测试一个单元的功能是否正确。一个好的单元测试应该独立、可重复、快速且专注。
  • 断言 (Assertion): 测试中的核心操作,用于判断实际结果是否符合预期。如果断言失败,测试就失败。
  • 测试替身 (Test Doubles): 包括Mock、Stub、Fake等,用于替换那些难以在测试环境中直接使用的依赖项(如数据库、网络请求、外部API),使测试更纯粹、更快速、更可控。

第二部分:unittest 框架详解

unittest是Python标准库中自带的单元测试框架,灵感来源于Java的JUnit。它功能全面,是学习单元测试的绝佳起点。

2.1 实战案例:使用unittest测试计算器类

让我们创建一个简单的计算器类,并为其编写全面的单元测试。

calculator.py (被测试的代码)

class Calculator:
    """一个简单的计算器类"""

    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 ZeroDivisionError("Cannot divide by zero.")
        return a / b

    def get_data_from_api(self, endpoint):
        """
        模拟一个需要网络请求的外部依赖。
        在真实场景中,这会调用requests.get等。
        """
        # 这里我们模拟一个外部API调用,返回一个数字
        import time
        time.sleep(0.1) # 模拟网络延迟
        if endpoint == "/number":
            return 42
        return 0

test_calculator_unittest.py (单元测试代码)

import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
    """Calculator类的测试套件"""

    def setUp(self):
        """在每个测试方法执行前运行,用于初始化测试环境"""
        self.calc = Calculator()

    def test_add_positive_numbers(self):
        """测试两个正数相加"""
        result = self.calc.add(3, 5)
        # assertEqual 是unittest中最常用的断言方法之一
        self.assertEqual(result, 8)

    def test_add_negative_numbers(self):
        """测试两个负数相加"""
        result = self.calc.add(-3, -5)
        self.assertEqual(result, -8)

    def test_subtract(self):
        """测试减法"""
        result = self.calc.subtract(10, 3)
        self.assertEqual(result, 7)

    def test_multiply(self):
        """测试乘法"""
        result = self.calc.multiply(4, 3)
        self.assertEqual(result, 12)

    def test_divide_normal(self):
        """测试正常除法"""
        result = self.calc.divide(10, 2)
        self.assertEqual(result, 5.0)

    def test_divide_by_zero_raises_exception(self):
        """测试除零时是否会抛出异常"""
        # 使用assertRaises上下文管理器来测试异常
        with self.assertRaises(ZeroDivisionError):
            self.calc.divide(10, 0)

    def test_divide_by_zero_with_message_check(self):
        """测试除零异常的详细信息"""
        with self.assertRaisesRegex(ZeroDivisionError, "Cannot divide by zero"):
            self.calc.divide(10, 0)

    def test_add_floats(self):
        """测试浮点数相加 (注意精度问题)"""
        result = self.calc.add(0.1, 0.2)
        # 对于浮点数,使用assertAlmostEqual来处理精度误差
        self.assertAlmostEqual(result, 0.3, places=7)

    def tearDown(self):
        """在每个测试方法执行后运行,用于清理资源"""
        # 在这个例子中,我们不需要做任何清理
        pass

if __name__ == '__main__':
    # unittest.main() 会自动发现并运行所有以'test_'开头的方法
    unittest.main()

运行测试:

在终端中执行以下命令:

python -m unittest test_calculator_unittest.py -v

输出示例:

test_add_floats (__main__.TestCalculator.test_add_floats)
... ok
test_add_negative_numbers (__main__.TestCalculator.test_add_negative_numbers)
... ok
test_add_positive_numbers (__main__.TestCalculator.test_add_positive_numbers)
... ok
test_divide_by_zero_raises_exception (__main__.TestCalculator.test_divide_by_zero_raises_exception)
... ok
test_divide_by_zero_with_message_check (__main__.TestCalculator.test_divide_by_zero_with_message_check)
... ok
test_divide_normal (__main__.TestCalculator.test_divide_normal)
... ok
test_multiply (__main__.TestCalculator.test_multiply)
... ok
test_subtract (__main__.TestCalculator.test_subtract)
... ok

----------------------------------------------------------------------
Ran 8 tests in 0.001s

OK

案例解析 (unittest):

  • class TestCalculator(unittest.TestCase): 定义一个测试类,它必须继承自unittest.TestCase
  • setUp(): 一个特殊方法,在类中每个测试方法运行前都会被调用。非常适合进行公共的初始化操作,如创建实例、连接数据库等。
  • tearDown(): 一个特殊方法,在类中每个测试方法运行后都会被调用。用于清理资源,如关闭文件、断开数据库连接等。
  • test_*(): 所有测试方法必须以test_开头,这样unittest才能自动找到并执行它们。
  • self.assertEqual(actual, expected): 最基本的断言,验证实际值是否等于期望值。
  • self.assertRaises(ExceptionType): 用于测试代码是否会抛出预期的异常。它是一个上下文管理器,需要用with语句包围。
  • self.assertRaisesRegex(ExceptionType, regex_pattern): 更精确地测试异常,不仅检查异常类型,还检查异常消息是否符合正则表达式。
  • self.assertAlmostEqual(first, second, places=7): 专门用于比较浮点数,places参数指定了比较的小数位数,以应对浮点数计算的精度问题。

第三部分:pytest 框架详解

pytest是一个第三方的、功能更强大、使用更便捷的测试框架。它的语法更简洁,插件生态更丰富,是目前Python社区的主流选择。

3.1 环境准备
pip install pytest
3.2 实战案例:使用pytest重写计算器测试

让我们用pytest的风格重写上面的测试。

test_calculator_pytest.py

import pytest
from calculator import Calculator

# Fixtures 是 pytest 的核心特性之一,用于提供测试数据或设置
@pytest.fixture
def calc():
    """提供一个Calculator实例给所有测试使用"""
    print("Setting up calculator instance...") # 这行会在每次测试前打印
    return Calculator()

def test_add_positive_numbers(calc): # calc 参数会自动注入fixture的返回值
    """测试两个正数相加"""
    assert calc.add(3, 5) == 8

def test_add_negative_numbers(calc):
    """测试两个负数相加"""
    assert calc.add(-3, -5) == -8

def test_divide_by_zero(calc):
    """测试除零异常"""
    with pytest.raises(ZeroDivisionError, match="Cannot divide by zero"):
        calc.divide(10, 0)

# 使用 @pytest.mark.parametrize 装饰器可以轻松实现参数化测试
@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
    (100, -50, 50),
])
def test_add_multiple_cases(calc, a, b, expected):
    """参数化测试多个加法用例"""
    assert calc.add(a, b) == expected

# 使用 pytest-mock 插件 (需 pip install pytest-mock) 可以更方便地进行Mock
def test_get_data_from_api_with_mock(calc, mocker):
    """
    测试依赖外部API的方法。
    我们将calc.get_data_from_api方法替换为一个mock,
    使其返回固定的值,从而不依赖真实的网络请求。
    """
    # 使用 mocker.patch 替换目标方法
    mock_return_value = 99
    mocker.patch.object(calc, 'get_data_from_api', return_value=mock_return_value)
    
    # 调用方法
    result = calc.get_data_from_api("/some_endpoint")
    
    # 验证结果
    assert result == mock_return_value
    # 验证方法被调用了一次
    calc.get_data_from_api.assert_called_once_with("/some_endpoint")

运行测试:

# 运行所有测试
pytest test_calculator_pytest.py

# 以详细模式运行
pytest test_calculator_pytest.py -v

案例解析 (pytest):

  • 简洁的语法pytest使用标准的assert语句进行断言,无需记住self.assertEqual等方法,代码更自然。
  • @pytest.fixture: 这是pytest最强大的功能之一。它定义了一个“装置”(fixture),可以为测试提供数据、设置环境或执行清理。calc fixture会在每个需要它的测试函数之前自动创建。
  • @pytest.mark.parametrize: 这个装饰器可以让你用一组测试数据多次运行同一个测试函数,是进行边界值测试和等价类划分的利器。
  • mocker fixture: 这是由pytest-mock插件提供的,是对unittest.mock的便捷封装。mocker.patch.object可以轻松地替换对象的属性或方法。

第四部分:测试替身 - Mock详解

当被测试的单元依赖于外部资源(如数据库、网络、文件系统)时,直接测试会很慢、不稳定且难以控制。Mock技术可以创建一个“假”的依赖对象,模拟其行为,从而隔离被测单元。

4.1 实战案例:使用Mock测试依赖外部服务的类

假设我们有一个天气报告服务,它依赖于一个外部的天气API。

weather_service.py

class WeatherAPI:
    """模拟一个外部天气API"""
    def get_temperature(self, city):
        # 这里会进行真实的网络请求
        import requests
        # response = requests.get(f"http://api.weather.com/temp/{city}")
        # return response.json()['temperature']
        # 为了演示,我们模拟一个网络请求
        import time
        time.sleep(1) # 模拟网络延迟
        if city.lower() == "beijing":
            return 25.0
        return 0.0

class WeatherReporter:
    """天气报告服务"""
    def __init__(self, weather_api):
        self.api = weather_api

    def get_report(self, city):
        temp = self.api.get_temperature(city)
        if temp > 30:
            return f"{city}的天气很热,温度为{temp}°C。"
        elif temp < 0:
            return f"{city}的天气很冷,温度为{temp}°C。"
        else:
            return f"{city}的天气很舒适,温度为{temp}°C。"

test_weather_service.py (使用Mock)

import pytest
from unittest.mock import Mock
from weather_service import WeatherReporter, WeatherAPI

def test_weather_reporter_hot():
    """测试高温报告"""
    # 1. 创建一个Mock对象,模拟WeatherAPI的行为
    mock_api = Mock(spec=WeatherAPI) # spec参数确保mock对象具有与原对象相同的接口
    mock_api.get_temperature.return_value = 35.0 # 设置mock的返回值

    # 2. 将mock对象注入到被测试的服务中
    reporter = WeatherReporter(weather_api=mock_api)

    # 3. 执行测试
    report = reporter.get_report("Beijing")

    # 4. 验证结果和行为
    assert "很热" in report
    assert "35.0°C" in report
    # 验证mock对象的get_temperature方法被正确调用
    mock_api.get_temperature.assert_called_once_with("Beijing")

def test_weather_reporter_cold():
    """测试低温报告"""
    mock_api = Mock(spec=WeatherAPI)
    mock_api.get_temperature.return_value = -5.0

    reporter = WeatherReporter(weather_api=mock_api)
    report = reporter.get_report("Harbin")

    assert "很冷" in report
    assert "-5.0°C" in report
    mock_api.get_temperature.assert_called_once_with("Harbin")

案例解析 (Mock):

  • Mock(spec=WeatherAPI): 创建一个WeatherAPI的模拟对象。spec参数非常重要,它可以防止你在测试中意外调用了一个不存在的方法。
  • mock_api.get_temperature.return_value = 35.0: 配置Mock对象的行为。当get_temperature方法被调用时,它会立即返回35.0,而不会执行任何真实的逻辑。
  • assert_called_once_with("Beijing"): 这是一个非常强大的断言,它不仅验证了get_temperature方法被调用了,还验证了它被调用时传入的参数是"Beijing"

第五部分:测试最佳实践
  1. 保持测试的独立性: 每个测试都应该能够独立运行,不依赖于其他测试的执行结果或顺序。
  2. 命名清晰: 测试方法的名称应该清楚地描述它在测试什么以及期望的结果是什么,例如test_add_positive_numbers_returns_correct_sum
  3. 测试边界条件: 不仅要测试正常情况,还要测试边界值(如0, 1, -1)、空值、最大值、最小值等。
  4. 使用Mock: 对于外部依赖,尽可能使用Mock对象,确保测试的稳定性和速度。
  5. 测量代码覆盖率: 使用pytest-cov等工具来查看测试覆盖了多少代码,努力提高覆盖率。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐