Python 测试教程:从入门到精通

目录

  1. 为什么需要测试?
  2. 测试的核心概念
    • 测试用例
    • 测试套件
    • 测试运行器
    • 断言
  3. Python 内置测试库:unittest
    • 一个简单的 unittest 示例
    • unittest 核心组件详解
    • setUptearDown
    • setUpClasstearDownClass
  4. 更简洁的选择:pytest
    • 安装 pytest
    • 一个简单的 pytest 示例
    • pytest 的核心优势
    • 常用的 pytest 插件
  5. 测试“不可控”的部分:Mocking
    • 什么是 Mock?
    • Python 的 unittest.mock
    • 使用 @patch 装饰器
  6. 测试驱动开发 简介
  7. 最佳实践与技巧
  8. 总结与学习路径

为什么需要测试?

测试是软件开发中保证代码质量的关键环节,其主要目的包括:

python 测试教程
(图片来源网络,侵删)
  • 保证代码质量:确保代码按预期工作,减少 Bug。
  • 提供安全网:当代码被修改或重构时,测试可以快速发现是否引入了新的错误(这被称为回归测试)。
  • 促进模块化设计:为了便于测试,你会倾向于编写更小、更独立、更解耦的函数和类。
  • 作为活的文档:测试用例展示了代码的使用方法和预期行为,是理解代码功能的好帮手。
  • 简化集成:可以独立地测试各个模块,然后再将它们集成起来测试,降低了复杂度。

测试的核心概念

在开始写代码前,了解几个基本概念很重要:

  • 测试用例:针对一个特定功能点或场景的最小测试单元,它通常包含:准备数据 -> 执行操作 -> 断言结果。
  • 测试套件:一组相关的测试用例的集合,用于对整个模块或应用程序进行系统性的测试。
  • 测试运行器:一个工具,用于发现、执行测试用例,并报告测试结果(成功、失败、错误等),Python 自带的 unittest 模块和第三方的 pytest 都是测试运行器。
  • 断言:在测试中,断言是检查某个条件是否为真的语句,如果条件为 False,测试就会失败,并抛出 AssertionError

Python 内置测试库:unittest

Python 标准库自带的 unittest 模块是一个功能强大的面向对象的测试框架,它的设计灵感来源于 Java 的 JUnit。

一个简单的 unittest 示例

假设我们有一个简单的计算器模块 calculator.py

# calculator.py
def add(a, b):
    """Add two numbers."""
    return a + b
def subtract(a, b):
    """Subtract b from a."""
    return a - b

我们为它编写测试 test_calculator_unittest.py

python 测试教程
(图片来源网络,侵删)
# test_calculator_unittest.py
import unittest
from calculator import add, subtract
class TestCalculator(unittest.TestCase):
    """Test cases for the calculator functions."""
    def test_add(self):
        """Test the add function."""
        self.assertEqual(add(1, 2), 3)
        self.assertEqual(add(-1, 5), 4)
        self.assertEqual(add(0, 0), 0)
    def test_subtract(self):
        """Test the subtract function."""
        self.assertEqual(subtract(5, 3), 2)
        self.assertEqual(subtract(2, 5), -3)
        self.assertEqual(subtract(10, 10), 0)
if __name__ == '__main__':
    unittest.main()

代码解析

  1. import unittest:导入测试框架。
  2. from calculator import add, subtract:导入被测试的函数。
  3. class TestCalculator(unittest.TestCase)::我们创建一个测试类,它必须继承自 unittest.TestCase,所有相关的测试方法都放在这个类里。
  4. def test_add(self)::每个测试方法都必须以 test_ 开头,这样测试运行器才能自动发现它。
  5. self.assertEqual(...): 这是最常用的断言方法,它会检查两个值是否相等,如果不等,测试失败。
  6. if __name__ == '__main__': unittest.main(): 这是一个标准的 Python 习语,表示当这个脚本被直接运行时,执行测试。

如何运行

在终端中,进入文件所在目录,运行:

python -m unittest test_calculator_unittest.py

或者直接运行脚本:

python 测试教程
(图片来源网络,侵删)
python test_calculator_unittest.py

你会看到输出,显示有多少测试通过,多少失败。

unittest 核心组件详解

  • 断言方法unittest.TestCase 提供了丰富的断言方法:
    • assertEqual(a, b): a == b
    • assertNotEqual(a, b): a != b
    • assertTrue(x): bool(x) is True
    • assertFalse(x): bool(x) is False
    • assertIs(a, b): a is b
    • assertIsNot(a, b): a is not b
    • assertIsNone(x): x is None
    • assertIsNotNone(x): x is not None
    • assertIn(a, b): a in b
    • assertNotIn(a, b): a not in b
    • assertRaises(Exception, func, *args, **kwargs): 检查 func 在调用时是否抛出了指定的异常。

setUptearDown

如果多个测试方法需要共享一些准备和清理工作,可以使用 setUptearDown

  • setUp(): 在每个测试方法执行之前运行,用于创建测试对象、准备数据等。
  • tearDown(): 在每个测试方法执行之后运行,用于清理资源,如关闭文件、数据库连接等。

示例

class TestStringMethods(unittest.TestCase):
    def setUp(self):
        print("\n[Run] setUp: 准备测试数据...")
        self.data = "hello world"
    def tearDown(self):
        print("[Run] tearDown: 清理测试数据...")
        # self.data = None # 可以在这里清理
    def test_upper(self):
        self.assertEqual(self.data.upper(), "HELLO WORLD")
    def test_isupper(self):
        self.assertFalse(self.data.isupper())

setUpClasstearDownClass

如果某些设置只需要在整个测试类执行一次,可以使用类方法 setUpClasstearDownClass

  • setUpClass(): 在测试类的第一个测试方法执行之前运行。
  • tearDownClass(): 在测试类的最后一个测试方法执行之后运行。
class TestDatabase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print("\n[Run] setUpClass: 初始化数据库连接...")
        # cls.db_connection = create_db_connection()
    @classmethod
    def tearDownClass(cls):
        print("[Run] tearDownClass: 关闭数据库连接...")
        # cls.db_connection.close()
    def test_user_insert(self):
        # 使用 cls.db_connection 进行测试
        pass
    def test_user_query(self):
        # 使用 cls.db_connection 进行测试
        pass

更简洁的选择:pytest

unittest 很强大,但它的语法比较冗长。pytest 是一个更现代、更简洁、功能更丰富的第三方测试框架,已经成为 Python 社区的主流选择。

安装 pytest

pip install pytest

一个简单的 pytest 示例

我们用 pytest 来重写上面的计算器测试,测试文件可以命名为 test_calculator_pytest.pypytest 推荐 test__test.py 命名)。

# test_calculator_pytest.py
from calculator import add, subtract
def test_add():
    assert add(1, 2) == 3
    assert add(-1, 5) == 4
    assert add(0, 0) == 0
def test_subtract():
    assert subtract(5, 3) == 2
    assert subtract(2, 5) == -3
    assert subtract(10, 10) == 0

代码解析

  • 无需继承:测试函数不需要继承任何类。
  • 简单的断言:直接使用 Python 内置的 assert 语句。pytest 会智能地分析断言失败的原因,提供非常清晰的错误报告。
  • 函数命名:测试函数名以 test_ 开头即可。

如何运行

在终端中,进入项目根目录,直接运行 pytest

pytest

pytest 会自动发现并运行当前目录及其子目录下所有符合命名规范的测试文件。

pytest 的核心优势

  1. 简洁的语法:无需编写类和继承,assert 语句更直观。
  2. 强大的发现机制:自动发现测试文件和测试函数/方法。
  3. 详尽的错误报告:当测试失败时,pytest 会显示变量值、调用栈等大量信息,非常便于调试。
  4. 丰富的插件生态pytest 有大量的插件,可以扩展其功能,
    • pytest-cov:生成代码覆盖率报告。
    • pytest-xdist:并行运行测试,加速测试过程。
    • pytest-mock:提供更方便的 Mock 功能。
    • pytest-sugar:美化测试输出界面。

常用的 pytest 插件

安装插件

pip install pytest-cov pytest-xdist

运行带覆盖率的测试

pytest --cov=calculator

这会运行测试并生成 calculator.py 的覆盖率报告。


测试“不可控”的部分:Mocking

在测试中,我们常常会遇到一些“不可控”或“昂贵”的外部依赖,

  • 数据库
  • 网络请求
  • 文件系统
  • 时间

Mocking(模拟)就是创建这些依赖的替身(Mock 对象),以便在隔离的环境中测试我们的代码,我们控制这个替身的行为,让它返回我们想要的结果,从而避免真实依赖带来的问题。

Python 的 unittest.mock

Python 3.3+ 自带了 unittest.mock 库,pytest 也完全兼容它。

示例: 假设我们有一个 UserService,它从数据库获取用户信息。

# user_service.py
import requests
class UserService:
    def get_user_name(self, user_id):
        try:
            response = requests.get(f"https://api.example.com/users/{user_id}")
            response.raise_for_status()  # 如果请求失败则抛出异常
            return response.json()['name']
        except requests.exceptions.RequestException:
            return None

我们不想在每次测试时都真的发送一个 HTTP 请求,我们可以 mock requests.get

# test_user_service.py
import unittest
from unittest.mock import patch, MagicMock
from user_service import UserService
class TestUserService(unittest.TestCase):
    def setUp(self):
        self.service = UserService()
    @patch('user_service.requests.get') # 关键: patch 目标是 '模块名.对象名'
    def test_get_user_name_success(self, mock_get):
        # 1. 配置 Mock 对象的行为
        mock_response = MagicMock()
        mock_response.json.return_value = {'name': 'Alice'}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
        # 2. 执行被测试的代码
        name = self.service.get_user_name(123)
        # 3. 断言
        self.assertEqual(name, 'Alice')
        mock_get.assert_called_once_with('https://api.example.com/users/123') # 检查是否被正确调用
    @patch('user_service.requests.get')
    def test_get_user_name_failure(self, mock_get):
        # 1. 配置 Mock 对象模拟网络错误
        mock_get.side_effect = requests.exceptions.RequestException("Network error")
        # 2. 执行
        name = self.service.get_user_name(123)
        # 3. 断言
        self.assertIsNone(name)
        mock_get.assert_called_once_with('https://api.example.com/users/123')
if __name__ == '__main__':
    unittest.main()

代码解析

  • @patch('user_service.requests.get'): 这是一个装饰器,它会在测试方法执行前,用 MagicMock 对象替换 user_service 模块中的 requests.get 函数,并将这个 Mock 对象作为参数传入测试方法(这里是 mock_get)。
  • mock_response = MagicMock(): 创建一个通用的 Mock 对象。
  • mock_response.json.return_value = ...: 配置 Mock 对象的方法调用后的返回值,当调用 mock_response.json() 时,它会返回 {'name': 'Alice'}
  • mock_get.return_value = mock_response: 配置 mock_get 这个 Mock 对象在被调用时,返回我们预设好的 mock_response
  • mock_get.side_effect = ...: side_effect 更强大,可以设置一个异常,当 Mock 对象被调用时,会抛出这个异常。
  • mock_get.assert_called_once_with(...): 这是一个断言,用于验证 requests.get 是否以预期的参数被精确地调用了一次。

测试驱动开发 简介

测试驱动开发是一种开发方法论,流程如下:

  1. :先写一个失败的测试(Red),因为功能还没实现,所以测试必然会失败。
  2. 绿:编写最少的、最简单的代码,让这个测试通过(Green),此时只关心让测试通过,不考虑代码质量。
  3. 重构:在测试的“安全网”下,优化和重构代码,使其更清晰、更高效,同时确保所有测试仍然通过。

TDD 的好处

  • 代码质量高,可维护性好。
  • 测试覆盖率高,代码健壮。
  • 设计更清晰,因为你会从“如何使用这个功能”的角度去思考。

最佳实践与技巧

  1. 保持测试的独立性:每个测试都应该可以独立运行,不依赖于其他测试的执行顺序或状态。

  2. 测试应该快速:单元测试应该在几秒钟内运行完毕,慢速的测试(如集成测试)应该与快速测试分开。

  3. AAA 模式: Arrange(准备), Act(执行), Assert(断言),这是一种组织测试代码的清晰方式。

    def test_something():
        # Arrange: 准备测试数据和环境
        input_data = ...
        expected = ...
        # Act: 执行被测试的代码
        result = my_function(input_data)
        # Assert: 断言结果
        assert result == expected
  4. 测试思想,而非实现:测试代码应该验证其行为(它做了什么),而不是它如何做的,避免测试私有方法或具体的内部实现细节,因为这样会使测试在重构时变得脆弱。

  5. 命名清晰:测试函数/方法的名称应该清晰地描述它所测试的场景。test_login_with_valid_credentials_and_redirect_to_dashboard()

  6. 持续集成:将测试集成到 CI/CD 流程中(如 GitHub Actions, Jenkins),每次代码提交都自动运行测试,确保代码库的健康。


总结与学习路径

特性 unittest pytest
来源 Python 标准库 第三方库
语法 基于类,需要继承 TestCase,使用特定断言方法 基于函数/方法,直接用 assert,更简洁
发现机制 需要手动指定文件或类 自动发现 test_*.py*_test.py 文件
断言报告 标准错误信息 详细的、可读性强的错误报告,显示变量值等
Fixture setUp, tearDown @pytest.fixture,更强大、更灵活
插件生态 较少 非常丰富,是 pytest 的巨大优势

学习建议

  1. pytest 开始:对于大多数 Python pytest 是更现代、更高效的选择,它的学习曲线更平缓,能让你更快地写出好用的测试。
  2. 掌握核心:深入理解 assertFixtureMock,这三者是测试的基石。
  3. 实践,实践,再实践:为你自己的每一个小项目、每一个函数编写测试,只有在实际项目中使用,才能真正理解测试的价值和技巧。
  4. 阅读优秀项目的测试代码:去 GitHub 上找一些你喜欢的开源项目(如 Flask, Requests, Pandas),阅读它们的测试代码,学习别人的测试风格和组织方式。

希望这份教程能帮助你开启 Python 测试之旅!