Skip to content

Python unittest:编写并运行单元测试(完整指南)

Updated on

你写了一个处理用户数据的函数。用少量输入手动测试时一切正常。然后同事改了一个你依赖的辅助函数,而你的代码开始悄悄返回错误结果——整整三周都没人发现。这个 bug 最终进入生产环境,客户记录被破坏。根因只是一个没人对既有行为做验证的一行改动。这正是单元测试要避免的失败模式。Python 标准库自带 unittest——一个功能完整的测试框架,可以在回归问题进入生产前将其捕获,并且完全不依赖第三方库。

📚

什么是 unittest,为什么要用它?

unittest 是 Python 内置的测试框架,建模自 Java 的 JUnit。从 Python 2.1 起它就属于标准库,这意味着每个 Python 安装都自带它:不需要 pip install,不需要依赖管理。你编写测试类、定义测试方法,然后从命令行运行。

单元测试用于验证代码中的独立单元(函数、方法、类)在隔离条件下是否按预期工作。当每个单元单独都正确时,把它们集成起来就更不容易产生隐藏 bug。

下面是 unittest 开箱即用提供的能力:

  • 测试用例类(Test case classes),支持自动测试发现
  • 丰富的断言方法(相等、真值、异常、警告等)
  • setUp/tearDown 钩子,支持方法级与类级
  • 测试套件(Test suites),用于组织和分组测试
  • Mocking:通过 unittest.mock(Python 3.3 加入)
  • 命令行测试运行器,支持输出详细程度控制

你的第一个单元测试

从一个待测函数和对应的测试类开始。

# calculator.py
def add(a, b):
    return a + b
 
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
# test_calculator.py
import unittest
from calculator import add, divide
 
class TestCalculator(unittest.TestCase):
 
    def test_add_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)
 
    def test_add_negative_numbers(self):
        self.assertEqual(add(-1, -1), -2)
 
    def test_add_zero(self):
        self.assertEqual(add(0, 0), 0)
 
    def test_divide_normal(self):
        self.assertEqual(divide(10, 2), 5.0)
 
    def test_divide_by_zero_raises(self):
        with self.assertRaises(ValueError):
            divide(10, 0)
 
if __name__ == "__main__":
    unittest.main()

运行:

python -m unittest test_calculator.py -v

输出:

test_add_negative_numbers (test_calculator.TestCalculator) ... ok
test_add_positive_numbers (test_calculator.TestCalculator) ... ok
test_add_zero (test_calculator.TestCalculator) ... ok
test_divide_by_zero_raises (test_calculator.TestCalculator) ... ok
test_divide_normal (test_calculator.TestCalculator) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

所有以 test_ 开头的方法都会被自动发现并执行。测试类继承自 unittest.TestCase,后者提供了所有断言方法。

断言方法速查

unittest.TestCase 包含一套完整的断言方法。每个断言在检查不通过时都会给出清晰的失败信息。

相等与身份断言

MethodChecksExample
assertEqual(a, b)a == bself.assertEqual(result, 42)
assertNotEqual(a, b)a != bself.assertNotEqual(result, 0)
assertIs(a, b)a is bself.assertIs(singleton, instance)
assertIsNot(a, b)a is not bself.assertIsNot(obj1, obj2)
assertIsNone(x)x is Noneself.assertIsNone(result)
assertIsNotNone(x)x is not Noneself.assertIsNotNone(user)

布尔与成员关系断言

MethodChecksExample
assertTrue(x)bool(x) is Trueself.assertTrue(is_valid)
assertFalse(x)bool(x) is Falseself.assertFalse(has_errors)
assertIn(a, b)a in bself.assertIn("admin", roles)
assertNotIn(a, b)a not in bself.assertNotIn("deleted", status)
assertIsInstance(a, b)isinstance(a, b)self.assertIsInstance(result, dict)

数值与集合断言

MethodChecksExample
assertAlmostEqual(a, b)round(a-b, 7) == 0self.assertAlmostEqual(0.1 + 0.2, 0.3)
assertGreater(a, b)a > bself.assertGreater(len(results), 0)
assertLess(a, b)a < bself.assertLess(latency, 1.0)
assertCountEqual(a, b)元素相同,顺序任意self.assertCountEqual([3,1,2], [1,2,3])
assertListEqual(a, b)列表相等self.assertListEqual(result, expected)
assertDictEqual(a, b)字典相等self.assertDictEqual(config, defaults)

异常与警告断言

import unittest
import warnings
 
class TestExceptions(unittest.TestCase):
 
    def test_raises_value_error(self):
        """assertRaises checks that the exception is raised."""
        with self.assertRaises(ValueError):
            int("not_a_number")
 
    def test_raises_with_message(self):
        """assertRaisesRegex checks both the exception and its message."""
        with self.assertRaisesRegex(ValueError, "invalid literal"):
            int("not_a_number")
 
    def test_warns_deprecation(self):
        """assertWarns checks that a warning is issued."""
        with self.assertWarns(DeprecationWarning):
            warnings.warn("old function", DeprecationWarning)

始终优先使用更具体的断言,而不是 assertTrue。与其写 self.assertTrue(result == 42),不如写 self.assertEqual(result, 42)。具体断言会给出清晰的失败信息,例如 42 != 41,而 assertTrue 只会提示 False is not true

setUp 和 tearDown:测试夹具(Fixtures)

多数测试都需要一些初始状态——数据库连接、临时文件或预配置对象。setUptearDown 会在每个测试方法前后运行,确保每个测试都有一个全新的起点。

import unittest
import os
import tempfile
 
class TestFileProcessor(unittest.TestCase):
 
    def setUp(self):
        """Runs before EACH test method."""
        self.test_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.test_dir, "data.txt")
        with open(self.test_file, "w") as f:
            f.write("line1\nline2\nline3\n")
 
    def tearDown(self):
        """Runs after EACH test method."""
        os.remove(self.test_file)
        os.rmdir(self.test_dir)
 
    def test_read_lines(self):
        with open(self.test_file, "r") as f:
            lines = f.readlines()
        self.assertEqual(len(lines), 3)
 
    def test_file_exists(self):
        self.assertTrue(os.path.exists(self.test_file))
 
    def test_first_line_content(self):
        with open(self.test_file, "r") as f:
            first_line = f.readline().strip()
        self.assertEqual(first_line, "line1")

每个测试方法都会调用一次 setUp。即使 test_read_lines 修改了文件,test_first_line_content 仍会看到原始内容,因为 setUp 会重新创建它。

setUpClass 和 tearDownClass:一次性初始化

有些资源创建成本很高——数据库连接、大型数据夹具、服务器进程。使用 setUpClasstearDownClass 在整个测试类级别只创建一次。

import unittest
import sqlite3
 
class TestDatabase(unittest.TestCase):
 
    @classmethod
    def setUpClass(cls):
        """Runs ONCE before all tests in this class."""
        cls.conn = sqlite3.connect(":memory:")
        cls.cursor = cls.conn.cursor()
        cls.cursor.execute("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT UNIQUE NOT NULL
            )
        """)
        cls.cursor.executemany(
            "INSERT INTO users (name, email) VALUES (?, ?)",
            [
                ("Alice", "alice@example.com"),
                ("Bob", "bob@example.com"),
                ("Charlie", "charlie@example.com"),
            ],
        )
        cls.conn.commit()
 
    @classmethod
    def tearDownClass(cls):
        """Runs ONCE after all tests in this class."""
        cls.conn.close()
 
    def test_user_count(self):
        self.cursor.execute("SELECT COUNT(*) FROM users")
        count = self.cursor.fetchone()[0]
        self.assertEqual(count, 3)
 
    def test_find_user_by_email(self):
        self.cursor.execute(
            "SELECT name FROM users WHERE email = ?",
            ("bob@example.com",),
        )
        name = self.cursor.fetchone()[0]
        self.assertEqual(name, "Bob")
 
    def test_unique_emails(self):
        with self.assertRaises(sqlite3.IntegrityError):
            self.cursor.execute(
                "INSERT INTO users (name, email) VALUES (?, ?)",
                ("Duplicate", "alice@example.com"),
            )
HookRunsDecoratorUse Case
setUp每个测试方法之前None创建全新对象、重置状态
tearDown每个测试方法之后None清理文件、重置 mock
setUpClass本类所有测试之前运行一次@classmethod数据库连接、昂贵夹具
tearDownClass本类所有测试之后运行一次@classmethod关闭连接、删除共享资源

使用 unittest.mock 进行 Mocking

真实应用会依赖数据库、API、文件系统和网络服务。你不希望单元测试去调用生产 API,也不希望测试必须依赖一个正在运行的数据库。unittest.mock 可以用可控的替身替换这些依赖。

基础 Mock 对象

from unittest.mock import Mock
 
# Create a mock object
api_client = Mock()
 
# Configure return values
api_client.get_user.return_value = {"id": 1, "name": "Alice"}
 
# Use it like a real object
user = api_client.get_user(user_id=1)
print(user)  # {'id': 1, 'name': 'Alice'}
 
# Verify it was called correctly
api_client.get_user.assert_called_once_with(user_id=1)

使用 @patch 进行替换(Patching)

@patch 装饰器会在测试期间替换某个模块中的对象。这是将被测单元与其依赖隔离开的主要工具。

# user_service.py
import requests
 
def get_user_name(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status()
    return response.json()["name"]
# test_user_service.py
import unittest
from unittest.mock import patch, Mock
from user_service import get_user_name
 
class TestUserService(unittest.TestCase):
 
    @patch("user_service.requests.get")
    def test_get_user_name_success(self, mock_get):
        """Mock the requests.get call to avoid hitting the real API."""
        mock_response = Mock()
        mock_response.json.return_value = {"id": 1, "name": "Alice"}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
 
        result = get_user_name(1)
 
        self.assertEqual(result, "Alice")
        mock_get.assert_called_once_with("https://api.example.com/users/1")
 
    @patch("user_service.requests.get")
    def test_get_user_name_http_error(self, mock_get):
        """Verify that HTTP errors propagate correctly."""
        import requests
        mock_get.return_value.raise_for_status.side_effect = (
            requests.exceptions.HTTPError("404 Not Found")
        )
 
        with self.assertRaises(requests.exceptions.HTTPError):
            get_user_name(999)

将 patch 作为上下文管理器使用

import unittest
from unittest.mock import patch
 
class TestConfig(unittest.TestCase):
 
    def test_reads_environment_variable(self):
        with patch.dict("os.environ", {"DATABASE_URL": "sqlite:///test.db"}):
            import os
            self.assertEqual(os.environ["DATABASE_URL"], "sqlite:///test.db")

MagicMock vs Mock

MagicMockMock 的子类,预先配置了魔术方法(__len____iter____getitem__ 等)。当被测代码会在 mock 对象上调用这些 dunder 方法时,用 MagicMock 更合适。

from unittest.mock import MagicMock
 
mock_list = MagicMock()
mock_list.__len__.return_value = 5
mock_list.__getitem__.return_value = "item"
 
print(len(mock_list))     # 5
print(mock_list[0])       # item

使用 side_effect 实现复杂行为

side_effect 可以让 mock 抛出异常、在连续调用时返回不同值,或执行自定义函数。

from unittest.mock import Mock
 
# Raise an exception
mock_db = Mock()
mock_db.connect.side_effect = ConnectionError("Database unreachable")
 
# Return different values on successive calls
mock_api = Mock()
mock_api.fetch.side_effect = [{"page": 1}, {"page": 2}, StopIteration]
 
print(mock_api.fetch())  # {'page': 1}
print(mock_api.fetch())  # {'page': 2}
 
# Custom function
def validate_input(x):
    if x < 0:
        raise ValueError("Negative input")
    return x * 2
 
mock_fn = Mock(side_effect=validate_input)
print(mock_fn(5))  # 10

测试发现(Test Discovery)

你不需要手动列出每个测试文件。Python 的测试发现会查找并运行符合命名约定的全部测试。

# Discover and run all tests in the current directory tree
python -m unittest discover
 
# Specify a start directory and pattern
python -m unittest discover -s tests -p "test_*.py"
 
# Verbose output
python -m unittest discover -v

测试发现会搜索匹配 test_*.py(默认模式)的文件,导入它们,并运行所有继承自 unittest.TestCase 的类。

推荐的项目结构

my_project/
    src/
        calculator.py
        user_service.py
        utils.py
    tests/
        __init__.py
        test_calculator.py
        test_user_service.py
        test_utils.py
    setup.py

在项目根目录运行全部测试:

python -m unittest discover -s tests -v

使用测试套件组织测试(Test Suites)

如果你需要更细粒度地控制运行哪些测试,可以手动构建测试套件。

import unittest
from test_calculator import TestCalculator
from test_user_service import TestUserService
 
def fast_tests():
    """Suite of tests that run quickly (no I/O, no network)."""
    suite = unittest.TestSuite()
    suite.addTest(TestCalculator("test_add_positive_numbers"))
    suite.addTest(TestCalculator("test_add_negative_numbers"))
    suite.addTest(TestCalculator("test_divide_normal"))
    return suite
 
def all_tests():
    """Full test suite."""
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()
    suite.addTests(loader.loadTestsFromTestCase(TestCalculator))
    suite.addTests(loader.loadTestsFromTestCase(TestUserService))
    return suite
 
if __name__ == "__main__":
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(fast_tests())

跳过测试与预期失败(Expected Failures)

有时测试只应在特定条件下运行——比如特定 OS、特定 Python 版本,或外部服务可用时。

import unittest
import sys
 
class TestPlatformSpecific(unittest.TestCase):
 
    @unittest.skip("Temporarily disabled while refactoring")
    def test_feature_under_construction(self):
        pass
 
    @unittest.skipIf(sys.platform == "win32", "Not supported on Windows")
    def test_unix_permissions(self):
        import os
        self.assertTrue(os.access("/tmp", os.W_OK))
 
    @unittest.skipUnless(sys.platform.startswith("linux"), "Linux only")
    def test_proc_filesystem(self):
        import os
        self.assertTrue(os.path.exists("/proc"))
 
    @unittest.expectedFailure
    def test_known_bug(self):
        """This test documents a known bug. It SHOULD fail."""
        self.assertEqual(1 + 1, 3)
DecoratorEffect
@unittest.skip(reason)始终跳过该测试
@unittest.skipIf(condition, reason)condition 为 True 时跳过
@unittest.skipUnless(condition, reason)除非 condition 为 True,否则跳过
@unittest.expectedFailure标记为预期失败;若意外通过则被报告为错误

使用 subTest 的参数化测试

用不同输入测试同一段逻辑很常见。与其为每个 case 写一个单独的测试方法,不如用 subTest 在一个方法里运行参数化断言。

import unittest
 
def is_palindrome(s):
    cleaned = s.lower().replace(" ", "")
    return cleaned == cleaned[::-1]
 
class TestPalindrome(unittest.TestCase):
 
    def test_palindromes(self):
        test_cases = [
            ("racecar", True),
            ("hello", False),
            ("A man a plan a canal Panama", True),
            ("", True),
            ("ab", False),
            ("madam", True),
            ("Nurses Run", True),
        ]
        for text, expected in test_cases:
            with self.subTest(text=text):
                self.assertEqual(is_palindrome(text), expected)

使用 subTest 时,某个 case 失败不会阻止其他 case 继续运行。输出会标识到底是哪一个子用例失败:

FAIL: test_palindromes (test_palindrome.TestPalindrome) (text='Nurses Run')
AssertionError: False != True

如果不使用 subTest,第一次失败就会终止整个方法,你也无法知道其他用例是否同样失败。

unittest vs pytest vs doctest

Python 有三种内置或常用的测试工具,各自适用于不同场景。

Featureunittestpytestdoctest
Included in stdlibYesNo (pip install)Yes
Test styleClass-based (TestCase)Function-based (plain def test_)Embedded in docstrings
Assertionsself.assertEqual, self.assertTrue, etc.Plain assert statementExpected output matching
FixturessetUp/tearDown, setUpClass@pytest.fixture with dependency injectionNone
Parameterized testssubTest (limited)@pytest.mark.parametrize (powerful)One example per docstring
Mockingunittest.mock (built-in)unittest.mock + monkeypatchNot applicable
Test discoverypython -m unittest discoverpytest (auto-discovers)python -m doctest module.py
Output on failureBasic diffDetailed diff with contextShows expected vs actual output
PluginsNone1000+ plugins (coverage, fixtures, etc.)None
Learning curveModerate (OOP patterns)Low (plain functions)Very low
Best forStandard library projects, no extra dependenciesMost Python projects, complex test setupsSimple examples in documentation

何时选择 unittest:

  • 你希望零外部依赖
  • 你的组织或项目已经在使用 unittest
  • 你需要基于类的测试组织方式
  • 你希望直接使用 unittest.mock 且无需额外配置

何时选择 pytest:

  • 你想要更简单的语法和更好的失败输出
  • 你需要更强的参数化或 fixture 体系
  • 你依赖 pytest 插件生态(coverage、async、Django 等)

何时选择 doctest:

  • 你想验证文档里的代码示例仍然可运行
  • 测试只是简单的输入/输出对

注意:pytest 能无需修改直接运行 unittest 风格的测试。很多团队会从 unittest 起步,然后切换到 pytest 作为 runner,同时保留已有测试类。

测试一个真实世界的类:完整示例

下面给出一个对购物车实现进行测试的完整示例。

# cart.py
class Product:
    def __init__(self, name, price):
        if price < 0:
            raise ValueError("Price cannot be negative")
        self.name = name
        self.price = price
 
class ShoppingCart:
    def __init__(self):
        self.items = []
 
    def add(self, product, quantity=1):
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        self.items.append({"product": product, "quantity": quantity})
 
    def total(self):
        return sum(
            item["product"].price * item["quantity"]
            for item in self.items
        )
 
    def remove(self, product_name):
        self.items = [
            item for item in self.items
            if item["product"].name != product_name
        ]
 
    def item_count(self):
        return sum(item["quantity"] for item in self.items)
# test_cart.py
import unittest
from cart import Product, ShoppingCart
 
class TestProduct(unittest.TestCase):
 
    def test_create_product(self):
        p = Product("Widget", 9.99)
        self.assertEqual(p.name, "Widget")
        self.assertAlmostEqual(p.price, 9.99)
 
    def test_negative_price_raises(self):
        with self.assertRaises(ValueError):
            Product("Bad", -5.00)
 
class TestShoppingCart(unittest.TestCase):
 
    def setUp(self):
        self.cart = ShoppingCart()
        self.apple = Product("Apple", 1.50)
        self.bread = Product("Bread", 3.00)
 
    def test_empty_cart_total(self):
        self.assertEqual(self.cart.total(), 0)
 
    def test_add_single_item(self):
        self.cart.add(self.apple)
        self.assertEqual(self.cart.item_count(), 1)
        self.assertAlmostEqual(self.cart.total(), 1.50)
 
    def test_add_multiple_items(self):
        self.cart.add(self.apple, quantity=3)
        self.cart.add(self.bread, quantity=2)
        self.assertEqual(self.cart.item_count(), 5)
        self.assertAlmostEqual(self.cart.total(), 10.50)
 
    def test_remove_item(self):
        self.cart.add(self.apple, quantity=2)
        self.cart.add(self.bread)
        self.cart.remove("Apple")
        self.assertEqual(self.cart.item_count(), 1)
        self.assertAlmostEqual(self.cart.total(), 3.00)
 
    def test_remove_nonexistent_item(self):
        self.cart.add(self.apple)
        self.cart.remove("Nonexistent")
        self.assertEqual(self.cart.item_count(), 1)
 
    def test_add_zero_quantity_raises(self):
        with self.assertRaises(ValueError):
            self.cart.add(self.apple, quantity=0)
 
    def test_add_negative_quantity_raises(self):
        with self.assertRaises(ValueError):
            self.cart.add(self.apple, quantity=-1)
 
if __name__ == "__main__":
    unittest.main()

unittest 的最佳实践

1. 每个测试方法聚焦一个概念

每个测试方法应验证一个逻辑概念。只要都在验证同一操作的不同方面,多条断言是没问题的。

# GOOD -- multiple assertions about the same operation
def test_user_creation(self):
    user = create_user("alice", "alice@example.com")
    self.assertEqual(user.name, "alice")
    self.assertEqual(user.email, "alice@example.com")
    self.assertIsNotNone(user.id)
 
# BAD -- testing unrelated things in one method
def test_everything(self):
    user = create_user("alice", "alice@example.com")
    self.assertEqual(user.name, "alice")
    users = list_all_users()
    self.assertGreater(len(users), 0)  # Unrelated assertion

2. 使用描述性测试名称

测试名称应描述场景与预期结果。CI 里一旦失败,光看名字就应该能看出哪里出了问题。

# GOOD
def test_divide_by_zero_raises_value_error(self):
    ...
 
def test_empty_cart_returns_zero_total(self):
    ...
 
# BAD
def test_divide(self):
    ...
 
def test1(self):
    ...

3. 测试必须相互独立

任何测试都不应依赖另一个测试的输出或执行顺序。每个测试都应该自己搭建状态,并在结束后自行清理。

4. 保持测试快速

单元测试应在毫秒级完成。如果测试需要数据库,就 mock 它;如果需要 API,就 mock 它。把慢的集成测试留到单独的测试套件里。

5. 测试边界情况

始终测试边界条件:空输入、0、None、超大输入、以及无效类型。

def test_edge_cases(self):
    test_cases = [
        ([], 0),          # empty list
        ([0], 0),         # single zero
        ([-1, -2], -3),   # all negative
        ([999999999], 999999999),  # large number
    ]
    for inputs, expected in test_cases:
        with self.subTest(inputs=inputs):
            self.assertEqual(sum(inputs), expected)

6. 不要测试实现细节

测试公共接口与行为,而不是内部状态或私有方法。即使你重构内部实现,测试也应该仍然通过。

在命令行运行测试

# Run a specific test file
python -m unittest test_calculator.py
 
# Run a specific test class
python -m unittest test_calculator.TestCalculator
 
# Run a specific test method
python -m unittest test_calculator.TestCalculator.test_add_positive_numbers
 
# Verbose output (shows each test name and result)
python -m unittest -v
 
# Discover all tests in a directory
python -m unittest discover -s tests -p "test_*.py" -v
 
# Stop on first failure (fail-fast mode)
python -m unittest -f

使用 RunCell 编写与调试测试

当你在 Jupyter notebooks 中开发(在数据科学与探索式编程里很常见)时,编写和运行单元测试会显得很别扭。notebook 以交互式 cell 执行,但 unittest 更偏向模块与测试 runner。你经常不得不在 notebook 与测试文件之间复制代码,导致交互反馈循环被打断。

RunCell (opens in a new tab) 是一个面向 Jupyter 的 AI agent,用于弥合这个差距。它可以为你在 notebook cell 中定义的函数生成兼容 unittest 的测试用例,在 notebook 环境内运行它们,并结合上下文解释失败原因。如果 mock 设置不正确或断言失败,RunCell 会检查当前的实时变量,告诉你实际值是什么,而不只是抛出断言错误信息。对于数据流水线(data pipelines),当你需要验证 DataFrame 的变换是否产生正确的输出形状与数值时,RunCell 可以帮你搭建测试结构与断言,让你把精力集中在逻辑本身,而不是样板代码上。

FAQ

unittest 和 pytest 有什么区别?

unittest 是 Python 内置的测试框架,提供基于类的 API。pytest 是第三方框架,使用普通函数与 assert 语句。pytest 有更丰富的插件生态和更好的失败输出,但需要安装;unittest 在任何能运行 Python 的地方都可用,且无需额外依赖。

如何在 unittest 中只运行一个测试方法?

使用命令 python -m unittest test_module.TestClass.test_method。例如:python -m unittest test_calculator.TestCalculator.test_add_positive_numbers。它只运行指定方法,不会执行文件中的其他测试。

setUp 和 setUpClass 有什么区别?

setUp 在每个测试方法前运行,为每次测试创建全新的状态。setUpClass 在该测试类的所有测试之前只运行一次,并且是一个 @classmethod。昂贵的初始化(如数据库连接)用 setUpClass;轻量的每测试初始化用 setUp

如何在 unittest 中 mock 外部 API?

使用 unittest.mock.patch 替换调用 API 的函数。要 patch “被使用的位置”的导入路径,而不是函数的定义位置。例如如果 user_service.py 导入并使用了 requests.get,应 patch user_service.requests.get,而不是 requests.get

pytest 能运行 unittest 测试吗?

可以。pytest 与 unittest 风格的测试类完全兼容。你可以在使用 unittest.TestCase 的项目中直接运行 pytest 而无需修改。这使得迁移可以渐进式进行:新测试用 pytest 风格写,旧的 unittest 测试继续保留。

如何测试函数会抛出异常?

self.assertRaises(ExceptionType) 当作上下文管理器使用。如果 with 块里的代码抛出指定异常则测试通过;如果没有抛出异常或抛出不同异常则失败。使用 assertRaisesRegex 还可以同时校验异常消息。

总结

Python 的 unittest 框架是一套随每个 Python 安装一起提供的完整测试工具箱。它提供测试用例类、丰富的断言方法、方法级与类级的 setup/teardown 钩子、通过 unittest.mock 提供的 mocking 能力,以及内置的测试发现。你无需安装任何东西,就能开始编写可靠的测试。

基础用法很直接:继承 TestCase,方法名以 test_ 开头,使用具体的断言方法,并通过 python -m unittest 运行。随着项目增长,引入 setUp/tearDown 来确保测试隔离,用 @patch mock 外部依赖,用 subTest 做参数化测试。把测试组织在 tests/ 目录下,让测试发现机制处理剩下的工作。

写测试会在前期花时间,但在后期能节省更多时间。每一次被单元测试捕获的回归,都意味着一次从未发生的生产事故、一次从未出现的客户投诉,以及一次从未开始的 debug 过程。无论你坚持使用 unittest 还是最终迁移到 pytest,你围绕 unittest 模式建立的测试习惯——隔离、清晰断言、mock 依赖、全面覆盖边界情况——都可以通用于 Python 测试的各个体系。

📚