26、面向对象测试:单元测试基础(unittest/pytest),Mock对象,测试驱动开发(TDD)入门

说实话,很多写面向对象代码的同学,一听到「测试」两个字就头大。我当年也是这样。总觉得写代码已经够累了,还要写测试?这不是给自己找事吗?直到有一次,我改了一个看似无关紧要的父类方法,结果导致三个子模块同时崩溃……那天晚上我加班到凌晨两点。嗯,从那以后,我再也不敢不写单元测试了。

今天我们就来聊聊面向对象中的测试。说白了,测试就是给你的代码买一份保险。你想想看,买保险的时候觉得浪费钱,出事的时候才知道真香。测试也是一样的道理。

单元测试到底测什么?

单元测试,测的是「单元」。在面向对象里,这个单元通常是一个类,或者一个方法。我个人习惯,一个类对应一个测试类,一个公开方法对应一个测试方法。

举个例子,假设你有一个 Calculator 类:

class Calculator:
    def add(self, a, b):
        return a + b

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

单元测试要测什么?不是测你代码能不能跑,而是测它的行为是否符合预期。比如:

  • add(1, 2) 是不是返回 3?
  • divide(10, 2) 是不是返回 5?
  • divide(1, 0) 是不是抛出了异常?

这些看起来很简单对吧?但就是这些简单的东西,能帮你拦住 90% 的回归 bug。

unittest vs pytest:我该选哪个?

Python 里有两个主流的测试框架:unittestpytest。我两个都用过,说说我的感受。

特性 unittest pytest
风格 Java 风格,类 + 方法 函数式,更 Pythonic
断言 self.assertEqual() assert 原生语句
fixture setUp() / tearDown() @pytest.fixture
参数化 需要 subTest 或第三方库 内置 @pytest.mark.parametrize
插件生态 一般 非常丰富

我个人更推荐 pytest。为什么?因为它的写法更简洁,出错信息也更友好。你看下面这个对比就明白了:

# unittest 写法
import unittest

class TestCalc(unittest.TestCase):
    def test_add(self):
        self.assertEqual(Calculator().add(1, 2), 3)

# pytest 写法
def test_add():
    assert Calculator().add(1, 2) == 3

看到了吗?pytest 直接用 assert,少写一堆样板代码。我在项目中带新人时,pytest 的上手速度明显比 unittest 快。

我的建议:新项目直接用 pytest。如果项目已经用了 unittest,也没必要强行迁移,两个框架可以共存。

Mock对象:让测试不再依赖外部

写面向对象测试时,最头疼的就是「依赖」。比如你的类里调用了数据库、调用了第三方 API、或者调用了另一个复杂的服务。这时候怎么办?

Mock 对象就是干这个的。它模拟一个假的对象,让你控制它的行为。

我曾经遇到过一个场景:一个订单服务类,内部调用了支付网关。每次跑测试都要真的扣钱……这谁受得了?后来我用 Mock 把支付网关替换掉,测试瞬间变快,而且再也不用担心测试环境把生产环境的钱扣了。

看个例子:

from unittest.mock import Mock

# 假设这是你的支付网关
class PaymentGateway:
    def charge(self, amount):
        # 真的去扣钱
        pass

class OrderService:
    def __init__(self, gateway):
        self.gateway = gateway

    def process_order(self, amount):
        result = self.gateway.charge(amount)
        if result['status'] == 'success':
            return "订单成功"
        return "订单失败"

# 测试时,用 Mock 替换真实的网关
def test_process_order_success():
    mock_gateway = Mock()
    mock_gateway.charge.return_value = {'status': 'success'}

    service = OrderService(mock_gateway)
    result = service.process_order(100)

    assert result == "订单成功"
    mock_gateway.charge.assert_called_once_with(100)

这里的关键点:

  • Mock() 创建了一个假对象
  • return_value 指定了调用方法时的返回值
  • assert_called_once_with 验证了方法是否被正确调用
避坑指南:我曾经犯过一个错误——过度使用 Mock。把每个依赖都 Mock 掉,结果测试跑得飞快,但全是假的。后来发现真实环境里根本跑不通。记住:Mock 只用来隔离外部依赖,不要 Mock 你自己的业务逻辑。

测试驱动开发(TDD)入门

TDD,全称 Test-Driven Development。说白了就是「先写测试,再写代码」。流程很简单:

  1. 红:写一个会失败的测试
  2. 绿:写最少的代码让测试通过
  3. 重构:优化代码,保持测试通过

我第一次接触 TDD 时觉得这简直是反人类。代码都没写,我怎么知道要测什么?但后来我硬着头皮试了一个月,发现真香。为什么?

因为 TDD 逼着你在写代码之前先想清楚「这个类到底要干什么」。你想想看,如果你连测试都写不出来,说明你根本就没想清楚需求。

来看一个 TDD 的实战例子。假设我们要开发一个 UserValidator 类,用来验证用户信息。

第一步:写一个失败的测试

def test_valid_user():
    validator = UserValidator()
    result = validator.validate("alice", "alice@example.com")
    assert result is True

这时候 UserValidator 还不存在,测试肯定失败。好,红牌。

第二步:写最少的代码让测试通过

class UserValidator:
    def validate(self, name, email):
        return True  # 先硬编码通过

测试通过了。但显然这个实现是假的。没关系,TDD 允许你「作弊」,只要测试通过就行。

第三步:写第二个测试,让代码变真实

def test_invalid_email():
    validator = UserValidator()
    result = validator.validate("alice", "not-an-email")
    assert result is False

这个测试会失败,因为我们的 validate 永远返回 True。好,现在我们需要写真正的逻辑了:

class UserValidator:
    def validate(self, name, email):
        if not name or not email:
            return False
        if "@" not in email:
            return False
        return True

两个测试都通过了。你看,代码是「被测试逼出来的」,而不是你凭空想出来的。

核心思想:TDD 不是测试方法,而是一种设计方法。它让你写出更可测试、更松耦合的代码。如果你发现某个类很难写测试,那通常意味着它的设计有问题。

面向对象测试的常见陷阱

  • 测试内部实现细节:不要测试私有方法,只测公开接口。否则改一次实现,测试就要全改。
  • 测试太脆弱:比如断言了某个方法的调用次数,其实这个次数根本不重要。我见过有人因为加了一行日志,测试就挂了……
  • 忽略边界条件:空值、负数、超大值、并发调用。这些才是 bug 的高发区。
一个小技巧:写测试时,先想想「如果这个类被一个傻子调用,他会怎么用?」然后针对那些「傻子用法」写测试。往往能发现很多隐藏问题。

本章知识体系

下面这张图总结了本章的核心逻辑,从测试框架到 Mock 再到 TDD,是一条完整的链路:

面向对象测试知识体系 测试框架 Mock对象 TDD流程 unittest / pytest 断言 / fixture / 参数化 替换外部依赖 控制返回值 / 验证调用 红 → 绿 → 重构 测试驱动设计 核心目标:让面向对象代码更健壮、可维护 测试不是负担,而是你的安全网

好了,关于面向对象测试的基础就聊到这里。记住:测试不是写代码的敌人,而是你的盟友。刚开始可能会觉得慢,但坚持一个月,你会发现代码质量明显提升,改代码时也更有底气了。

公众号:蓝海资料掘金营,微信deep3321