PHP项目中的Mockery与测试替身:优雅隔离依赖的实战指南
目录导读
为什么需要测试替身?
在PHP项目中,单元测试的核心原则是“隔离”——只测试当前单元(如一个类或方法),而不依赖外部数据库、API、文件系统或其他类,一个OrderService类可能依赖PaymentGateway和EmailSender,如果直接测试,每次运行都会产生真实的支付请求或发送邮件,这既不高效也不安全。

测试替身(Test Double) 就是用来替代真实依赖的简化对象,它能让我们:
- 控制输入(模拟返回特定值)
- 验证输出(检查方法是否被调用)
- 消除副作用(不操作真实资源)
常见的测试替身类型包括Dummy、Stub、Mock和Spy,而Mockery是PHP生态中最流行的测试替身框架之一。
Mockery是什么?
Mockery是一个PHP测试替身库,用于创建模拟(Mock)和存根(Stub)对象,它支持PHPUnit、Codeception等主流测试框架,以其流畅的API和强大的匹配能力著称,相比PHPUnit自带的createMock,Mockery提供了更细粒度的控制,
- 预期方法调用次数(
shouldReceive) - 指定参数匹配(
withArgs) - 设置返回值或异常(
andReturn/andThrow)
安装命令:
composer require --dev mockery/mockery
核心概念:Dummy、Stub、Mock、Spy
1 Dummy(哑元)
Dummy是一个占位对象,用于满足参数列表,但从不被实际使用。
$dummy = Mockery::mock(DependencyInterface::class); $service = new OrderService($dummy); // 只为了通过构造函数
2 Stub(存根)
Stub提供预设的返回值,用于控制外部依赖的行为。
$paymentGateway = Mockery::mock(PaymentGateway::class);
$paymentGateway->shouldReceive('charge')
->with(100)
->andReturn(true);
3 Mock(模拟)
Mock不仅返回预设值,还验证方法是否按预期被调用(次数、参数顺序等)。
$emailSender = Mockery::mock(EmailSender::class);
$emailSender->shouldReceive('send')
->with('user@example.com', 'Order Confirmed')
->once();
4 Spy(间谍)
Spy记录所有调用,测试结束后通过shouldHaveReceived断言验证。
$logger = Mockery::spy(Logger::class);
$orderService->process();
$logger->shouldHaveReceived('info')->with('Order processed successfully');
Mockery实战:创建与配置替身
1 基础Mock配置
use Mockery\Adapter\Phpunit\MockeryTestCase;
class OrderServiceTest extends MockeryTestCase
{
public function testProcessOrder()
{
// 创建替身
$gateway = Mockery::mock(PaymentGateway::class);
$gateway->shouldReceive('charge')
->with(Mockery::on(function ($amount) {
return $amount > 0;
}))
->andReturn(true);
// 注入并调用
$service = new OrderService($gateway);
$result = $service->process(100);
$this->assertTrue($result);
}
}
2 参数匹配器(Matchers)
Mockery支持丰富的匹配规则:
$mock->shouldReceive('setName')
->with(\Mockery::type('string')) // 类型匹配
->with(\Mockery::any()) // 任意值
->with(\Mockery::not('bad_value')) // 不等于
->with(\Mockery::subset(['id' => 1])); // 数组子集
高级用法:部分模拟与静态方法
1 部分模拟(Partial Mock)
当你想测试真实类,但只替换其中一部分方法时,使用makePartial:
$userModel = Mockery::mock(User::class)->makePartial();
$userModel->shouldReceive('save')->andReturn(true);
$userModel->name = 'Test'; // 真实属性
$userModel->process(); // 真实方法,但save被替换
2 模拟静态方法
需配合alias前缀:
$mock = Mockery::mock('alias:App\Helpers\Logger');
$mock->shouldReceive('error')->andReturnNull();
Logger::error('test'); // 实际调用的是Mock
3 模拟终结方法(Final Methods)
Mockery默认可以模拟final类/方法,但需注意PHP版本:
$mock = Mockery::mock(FinalClass::class);
$mock->shouldReceive('finalMethod')->andReturn('overridden');
常见陷阱与最佳实践
陷阱1:忘记调用Mockery::close()
每次测试后应清理Mockery状态,否则可能内存泄漏或影响后续测试,推荐继承MockeryTestCase或在tearDown()中关闭:
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
陷阱2:过度使用Mock
避免对所有依赖都创建Mock,否则测试变成“验证Mock行为”而非业务逻辑,建议:
- 对外部资源(数据库、API、文件系统)使用Mock
- 对内部值对象(如
Money、DateRange)直接使用真实类
陷阱3:Mock太严格
shouldReceive默认要求方法被调用一次,如果方法不被调用,测试会失败,改用zeroOrMoreTimes()或atLeast()->once():
$mock->shouldReceive('log')->zeroOrMoreTimes();
QA环节
Q1:Mockery与PHPUnit内置Mock有什么主要区别?
A:PHPUnit的Mock通过createMock创建,功能相对基础(如method、willReturn),Mockery提供更丰富的匹配器(withArgs、on)、更灵活的调用次数控制(twice、never),以及Spy模式,对于复杂测试场景(如链式调用、静态方法),Mockery更强大。
Q2:如何Mock一个包含构造函数的类?
A:如果不想执行构造函数,使用Mockery::mock(ClassName::class)默认会跳过构造函数,如果需要部分执行,可结合makePartial或通过constructor参数设置:
$mock = Mockery::mock(ClassName::class, [/* 构造参数 */]);
Q3:测试中如何验证Mock的方法被调用顺序?
A:使用ordered匹配器:
$mock->shouldReceive('firstMethod')->ordered();
$mock->shouldReceive('secondMethod')->ordered();
Q4:Mockery是否支持日志记录调用历史?
A:支持,使用spy创建的对象会自动记录所有调用,之后可通过shouldHaveReceived断言验证。
$spy->shouldHaveReceived('send')->withArgs(['email']);
Mockery作为PHP测试替身的利器,能帮助你构建高隔离性的单元测试,其核心在于区分Dummy、Stub、Mock和Spy的适用场景,并利用丰富的匹配器模拟复杂交互。好的测试不是测试Mock本身,而是通过Mock验证业务逻辑的边界条件。
掌握Mockery,让你的PHP项目测试更健壮、更有价值。