unittest 单元测试:Python 内置测试框架

FreeGuideOnline 最新 2026-06-16

unittest 单元测试:Python 内置测试框架

单元测试是高质量软件开发的基石,它能让你在修改代码后快速验证功能是否依然正常。Python 标准库自带的 unittest 模块提供了一个强大且无需额外安装的测试框架,非常适合初学者快速上手。

快速开始:你的第一个测试

创建一个名为 calculator.py 的文件,编写一个简单的加法函数:

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

接下来,在同一目录下创建 test_calculator.py,编写测试代码:

import unittest
from calculator import add

class TestCalculator(unittest.TestCase):
    def test_add_integers(self):
        result = add(2, 3)
        self.assertEqual(result, 5)

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

运行测试:

python test_calculator.py

终端输出一个点 . 表示测试通过。如果失败会显示 F 和详细错误信息。这就是 unittest 最基本的工作流程。

核心概念

unittest 框架围绕四个核心组件构建,理解它们有助于你掌握整个测试流程:

  • TestCase:测试用例,一个包含多个测试方法的类,每个方法验证一个功能点。必须继承 unittest.TestCase
  • TestSuite:测试套件,用于组合多个 TestCase 或 TestSuite,实现批量执行。
  • TestRunner:测试运行器,负责执行测试并将结果输出。命令行运行 unittest 时默认使用 TextTestRunner
  • TestLoader:测试加载器,自动发现并加载测试用例,常用于 discover 命令。
  • TestResult:测试结果对象,收集测试执行过程中的成功、失败、错误等信息。

常用断言方法

断言是测试的核心,用来验证实际结果是否与预期一致。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(Exc, func, *args) 调用 func 抛出指定异常
assertAlmostEqual(a, b) 浮点数近似相等(7位小数)

示例:测试异常和浮点数比较

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

class TestMath(unittest.TestCase):
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            divide(10, 0)

    def test_float_precision(self):
        result = 0.1 + 0.2
        self.assertAlmostEqual(result, 0.3, places=7)

测试夹具:setUp 与 tearDown

测试夹具允许你在每个测试方法执行前后进行准备和清理工作,例如连接数据库、创建临时文件等。

  • 方法级别setUptearDown 分别在每个测试方法运行前后执行。
  • 类级别setUpClasstearDownClass 在整个测试类运行前后执行(仅一次),需用 @classmethod 装饰。
  • 模块级别setUpModuletearDownModule 在模块所有测试运行前后执行(需定义为模块内函数)。
class TestWithFixtures(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """整个测试类运行前执行,例如建立数据库连接"""
        print("建立数据库连接")

    @classmethod
    def tearDownClass(cls):
        """整个测试类运行后执行,例如关闭连接"""
        print("关闭数据库连接")

    def setUp(self):
        """每个测试方法执行前运行,例如准备测试数据"""
        self.data = [1, 2, 3]

    def tearDown(self):
        """每个测试方法执行后运行,例如清理临时文件"""
        self.data.clear()

    def test_append(self):
        self.data.append(4)
        self.assertEqual(len(self.data), 4)

    def test_pop(self):
        self.data.pop()
        self.assertEqual(len(self.data), 2)

跳过测试与预期失败

有时某些测试需要根据条件跳过,或标记为预期失败(已知 bug 暂不修复),可通过装饰器实现。

import sys

class SkipExample(unittest.TestCase):
    @unittest.skip("暂时跳过该测试")
    def test_skipped(self):
        self.assertEqual(1, 2)

    @unittest.skipIf(sys.platform.startswith("win"), "Windows 平台跳过")
    def test_only_linux(self):
        self.assertEqual(10, 10)

    @unittest.expectedFailure
    def test_known_bug(self):
        self.assertEqual(1 + 1, 3)   # 失败但会被标记为期望失败

参数化测试:subTest

unittest 本身没有提供如 pytest 的参数化装饰器,但内置的 subTest 上下文管理器可以在一个测试方法中运行多个子测试,并能独立报告失败情况。

def power(base, exp):
    return base ** exp

class TestPower(unittest.TestCase):
    def test_powers(self):
        test_cases = [
            (2, 3, 8),
            (5, 0, 1),
            (4, -1, 0.25),
        ]
        for base, exp, expected in test_cases:
            with self.subTest(base=base, exp=exp):
                self.assertAlmostEqual(power(base, exp), expected)

如果某个子测试失败,其他子测试仍会继续执行,输出中会明确显示哪组参数导致失败。

测试发现:自动搜索测试用例

当项目变大,测试文件散布在多个目录时,可以使用 discover 命令自动查找并运行所有测试。默认规则是寻找文件名匹配 test_*.py 的模块。

# 从当前目录开始搜索
python -m unittest discover

# 指定起始目录、模式
python -m unittest discover -s tests -p 'test_*.py'

-s 指定起始目录,-p 指定文件名匹配模式。你可以将测试用例统一放在 tests/ 目录下,结构如下:

project/
├── calculator.py
└── tests/
    ├── __init__.py
    ├── test_calculator.py
    └── test_advanced.py

测试套件:手动组合测试

如果你需要精确控制执行哪些测试,可以创建 TestSuite 并手动添加测试。

import unittest
from tests.test_calculator import TestCalculator
from tests.test_advanced import TestAdvanced

def suite():
    suite = unittest.TestSuite()
    suite.addTest(TestCalculator('test_add_integers'))
    suite.addTest(TestAdvanced('test_power'))
    return suite

if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    runner.run(suite())

用 Mock 隔离外部依赖

单元测试应只关注当前代码逻辑,不依赖数据库、网络或文件系统。unittest.mock 模块提供了创建模拟对象的能力。

from unittest.mock import Mock, patch

def fetch_data(url):
    """模拟向外部 API 发起请求"""
    import requests
    response = requests.get(url)
    return response.json()

class TestFetch(unittest.TestCase):
    @patch('module_name.requests.get')   # 用 patch 替换目标对象
    def test_fetch_data(self, mock_get):
        # 配置模拟对象的行为
        mock_response = Mock()
        mock_response.json.return_value = {'key': 'value'}
        mock_get.return_value = mock_response

        result = fetch_data('http://example.com/api')
        self.assertEqual(result, {'key': 'value'})
        mock_get.assert_called_once_with('http://example.com/api')

patch 可以将依赖替换为 Mock 对象,你可以设定返回值,并验证调用参数,从而在无真实网络的情况下完成测试。

结合覆盖率达到更高质量的测试

虽然 coverage.py 不是标准库,但它是常用的衡量测试覆盖率的工具。安装后与 unittest 配合使用:

pip install coverage
coverage run -m unittest discover
coverage report -m          # 命令行报告
coverage html               # 生成 HTML 报告

这样你就能直观看到哪些代码行被测试执行过,哪些还需要补充测试。

总结

unittest 是 Python 开箱即用的测试框架,足以覆盖大部分测试需求。从简单的断言到复杂的 Mock,再到自动发现与测试套件,它提供了一套完整的测试工作流。掌握它,能让你更有信心地重构和扩展代码,构建更加可靠的软件。