Mocking、Stubbing、Spying 和 Faking 在 PHP 中:实用指南(含 Sandbox 示例)

发布: (2025年12月3日 GMT+8 03:00)
8 min read
原文: Dev.to

Source: Dev.to

现代 PHP 应用依赖于许多外部组件:API、数据库、文件系统、随机数生成器、时间提供者以及与外部世界通信的服务。

在编写测试时,你几乎不想与这些真实服务交互。这样做会让测试变慢、不可预测,并且难以在隔离环境中运行。

这就是测试双(test double)发挥作用的地方。

测试双是指在测试期间替代真实依赖的任何代替对象。PHP 提供了多种创建这些对象的方式——手动实现、使用像 Mockery 这样的库,或使用 PHPUnit 内置的 mocking 工具。

在本文中你将学习

  • Mock、Stub、Spy、Fake 实际是什么
  • 它们的区别以及何时使用每一种
  • 如何在纯 PHP 中构建它们
  • 如何测试依赖外部服务的代码
  • 开发者在 mock 时常犯的错误
  • 可以直接在 onlinephp.io 上运行的简单示例

四种主要的测试双类型

测试双有不同的分类。每一种都有非常具体的用途。

Stub

Stub 返回预定义的值。它不关心如何被调用——只会返回响应。

Mock

Mock 期望特定的调用(方法名、参数)。如果预期的调用没有发生,测试将失败。

Fake

Fake 是一种轻量级的替代实现。它的行为类似于真实对象,但实现更简化。

Spy

Spy 记录方法调用以供后续检查。与 Mock 不同,Spy 不会提前强制期望。

了解自己需要哪一种可以显著提升测试的清晰度。

沙盒示例:测试不同类型的 PHP 测试双

此沙盒演示了 PHP 中四种主要的测试双:

  • Stub – 返回固定、确定的值且不检查调用方式
  • Mock – 验证方法是否以期望的参数被调用
  • Fake – 提供轻量级的内存实现,模拟真实行为
  • Spy – 记录方法调用以供后续检查

你可以直接在浏览器中运行此示例:

expectedAmount = $expectedAmount;
    }

    public function charge(int $amount): bool {
        $this->called = true;
        $this->chargedAmount = $amount;
        return true;
    }

    public function verify(): bool {
        return $this->called && $this->chargedAmount === $this->expectedAmount;
    }
}

// -------------------------------------
// 3) Fake: stores transactions in memory
// -------------------------------------
class PaymentGatewayFake implements PaymentGateway {
    public array $transactions = [];

    public function charge(int $amount): bool {
        $this->transactions[] = [
            'amount' => $amount,
            'time'   => date('H:i:s'),
        ];
        return true;
    }
}

// -------------------------------------
// 4) Spy: records method calls for later inspection
// -------------------------------------
class PaymentGatewaySpy implements PaymentGateway {
    public array $calls = [];

    public function charge(int $amount): bool {
        $this->calls[] = $amount;
        return true;
    }
}

// -------------------------------------
// Function under test
// -------------------------------------
function processOrder(PaymentGateway $gateway, int $amount): bool {
    return $gateway->charge($amount);
}

// -------------------------------------
// Run examples
// -------------------------------------

echo "=== STUB ===\n";
$stub = new PaymentGatewayStub();
echo processOrder($stub, 1000) ? "Order OK\n\n" : "Order FAIL\n\n";

echo "=== MOCK ===\n";
$mock = new PaymentGatewayMock(500);
processOrder($mock, 500);
echo $mock->verify() ? "Mock expectations met!\n\n" : "Mock expectations NOT met!\n\n";

echo "=== FAKE ===\n";
$fake = new PaymentGatewayFake();
$fake->charge(300);
$fake->charge(400);
echo "Fake stored " . count($fake->transactions) . " transactions:\n";
print_r($fake->transactions);
echo "\n";

echo "=== SPY ===\n";
$spy = new PaymentGatewaySpy();
$spy->charge(100);
$spy->charge(200);
echo "Spy recorded calls:\n";
print_r($spy->calls);

深入理解四种测试双

虽然简短的定义已经提供了概览,但让我们更深入地探讨每一种类型,以了解它们的目的以及何时使用。

Stub

Stub 是一个简单对象,为方法调用提供预定义的响应。它不关心被测系统如何使用它——唯一的职责是返回一致的数据。

使用场景

  • 用确定性的响应替代慢速或不可靠的依赖(如 API 或数据库)。
  • 测试依赖特定返回值的逻辑,而不触发副作用。

示例情境
你想测试支付处理方法,但不想真的去刷信用卡。Stub 可以对所有收费返回 true,让测试专注于处理逻辑本身。

Mock

Mock 比 Stub 更严格。它期望特定的交互,例如特定的方法被以精确的参数调用。如果这些期望未被满足,测试将失败。

使用场景

  • 验证代码是否正确地与外部服务交互。
  • 断言副作用,例如确保发送了邮件或记录了日志。

示例情境
你需要检查 charge() 是否以确切的金额被调用。如果系统使用了不同的金额或根本没有调用,Mock 将使测试失败。

Fake

Fake 是一个功能完整但简化的依赖实现。与 Stub 不同,Fake 实现了实际的逻辑,通常在内存中完成,而不访问真实的外部系统。

使用场景

  • 在不使用生产资源的情况下模拟复杂行为。
  • 运行需要真实交互但不应依赖慢速或脆弱基础设施的测试。

示例情境
创建一个内存中的支付网关来存储交易记录。你的应用可以多次收费、检查交易历史并运行完整的工作流——全部不需要连接真实的支付系统。

Spy

Spy 记录在测试期间它是如何被使用的信息。与 Mock 不同,Spy 不会提前强制期望;你在事后检查记录的调用以断言行为。

使用场景

  • 监控方法调用及其参数以进行验证。
  • 捕获交互的额外细节而不立即使测试失败。

示例情境
验证 charge() 被调用了两次且使用了特定的金额。Spy 会保存所有调用记录,你可以在事后进行断言,这使其非常适合后置验证。

结论

Mock 和 Fake 是现代 PHP 测试中的关键技术。正确使用它们可以让你:

  • 在隔离环境中测试复杂代码
  • 避免慢速且不可靠的外部调用
  • 编写快速且确定性的测试
  • 模拟在真实系统中难以触发的边缘情况

通过选择合适的测试双——Stub、Mock、Fake 或 Spy——你可以保持测试套件的可维护性、可表达性和可靠性。

Back to Blog

相关文章

阅读更多 »