PHP项目Mockery与测试替身

wen PHP项目 2

PHP项目中的Mockery与测试替身:优雅隔离依赖的实战指南

目录导读

  1. 为什么需要测试替身?
  2. Mockery是什么?
  3. 核心概念:Dummy、Stub、Mock、Spy
  4. Mockery实战:创建与配置替身
  5. 高级用法:部分模拟与静态方法
  6. 常见陷阱与最佳实践
  7. QA环节

为什么需要测试替身?

在PHP项目中,单元测试的核心原则是“隔离”——只测试当前单元(如一个类或方法),而不依赖外部数据库、API、文件系统或其他类,一个OrderService类可能依赖PaymentGatewayEmailSender,如果直接测试,每次运行都会产生真实的支付请求或发送邮件,这既不高效也不安全。

PHP项目Mockery与测试替身

测试替身(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
  • 内部值对象(如MoneyDateRange)直接使用真实类

陷阱3:Mock太严格

shouldReceive默认要求方法被调用一次,如果方法不被调用,测试会失败,改用zeroOrMoreTimes()atLeast()->once()

$mock->shouldReceive('log')->zeroOrMoreTimes();

QA环节

Q1:Mockery与PHPUnit内置Mock有什么主要区别?

A:PHPUnit的Mock通过createMock创建,功能相对基础(如methodwillReturn),Mockery提供更丰富的匹配器(withArgson)、更灵活的调用次数控制(twicenever),以及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项目测试更健壮、更有价值。

抱歉,评论功能暂时关闭!