Mocking、Stubbing、Spying 和 Faking 在 PHP 中:实用指南(含 Sandbox 示例)
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——你可以保持测试套件的可维护性、可表达性和可靠性。