PHP单元测试mock怎么用

wen PHP项目 2

PHP单元测试Mock怎么用?从入门到实战,一篇讲透

目录导读

  1. 为什么你需要理解Mock?
  2. Mock的核心概念与术语澄清
  3. PHPUnit中Mock的三种创建方式
  4. 实战:Mock方法与返回值控制
  5. 进阶:Mock约束与参数验证
  6. 常见陷阱与最佳实践
  7. FAQ高频问答
  8. 从会用走向精通

为什么你需要理解Mock?

在真实项目中,你的代码往往依赖外部服务:数据库、REST API、文件系统或第三方SDK,如果每次跑单元测试都要连接这些真实资源,测试会变得缓慢、脆弱且不可控,Mock(模拟对象)技术应运而生——它让你用替身对象替换真实依赖,并精确控制其行为。

PHP单元测试mock怎么用

打个比方:测试汽车刹车系统,你不需要真的开到马路上,而是在台架上用假轮胎、假路面模拟,Mock就是测试中的“假路面”。


Mock的核心概念与术语澄清

术语 含义 举例
Stub(桩) 只返回预设数据,不验证调用 $mock->method('getPrice')->willReturn(100)
Mock(模拟) 桩 + 验证方法是否被调用 ->expects($this->once())->method('save')
Spy(间谍) 记录实际调用,事后断言 部分框架支持,PHPUnit用Mock实现

关键区分:Stub关注“返回什么”,Mock还要关注“怎么被调用的”。


PHPUnit中Mock的三种创建方式

PHPUnit框架提供了灵活且链式调用的API,以下是标准用法:

1 基础Mock(无参数)

$mock = $this->createMock(UserRepository::class);

2 完整MockBuilder(精细控制)

$mock = $this->getMockBuilder(UserRepository::class)
    ->disableOriginalConstructor()  // 禁用原构造函数
    ->onlyMethods(['findByEmail'])  // 只mock指定方法
    ->addMethods(['sendWelcomeEmail']) // 添加不存在的方法
    ->getMock();

3 上下文标记(PHPUnit 8+)

$mock = $this->createMock(LoggerInterface::class);

差异总结createMock是快捷方式,自动禁用构造函数;getMockBuilder提供更多定制选项,适合复杂场景。


实战:Mock方法与返回值控制

假设我们有一个OrderService,依赖PaymentGateway

class PaymentGateway {
    public function charge($amount) {
        // 真实调用第三方API
    }
}
class OrderService {
    private $gateway;
    public function __construct(PaymentGateway $gateway) {
        $this->gateway = $gateway;
    }
    public function placeOrder($amount) {
        $this->gateway->charge($amount);
    }
}

测试代码:

public function testOrderPlaced() {
    $gatewayMock = $this->createMock(PaymentGateway::class);
    $gatewayMock->expects($this->once())
                ->method('charge')
                ->with(99.99)  // 精确参数
                ->willReturn(true);  // 设置返回值
    $service = new OrderService($gatewayMock);
    $result = $service->placeOrder(99.99);
    $this->assertTrue($result);
}

多返回值

$mock->method('getStatus')
     ->willReturnOnConsecutiveCalls('pending', 'paid', 'shipped');

异常抛出

$mock->method('charge')
     ->willThrowException(new \Exception('Payment failed'));

进阶:Mock约束与参数验证

1 使用约束匹配参数

$mock->expects($this->once())
     ->method('search')
     ->with(
         $this->stringContains('keyword'),
         $this->greaterThan(50)
     );

其他常用约束:

  • $this->anything() — 任何值
  • $this->arrayHasKey('name') — 数组键
  • $this->callback(fn($value) => ...) — 自定义逻辑
  • $this->count(5) — 数组长度

2 验证调用次数

// 精确一次
$this->once()
// 至少两次
$this->atLeast(2)
// 从未调用
$this->never()
// 调用顺序(谨慎使用)
$this->at(0)

3 回调动态响应

$mock->method('process')
     ->willReturnCallback(function ($input) {
         return $input * 2;
     });

常见陷阱与最佳实践

❌ 陷阱1:Mock私有方法

PHPUnit默认无法mock私有/受保护方法,解决方案:将依赖通过接口注入,或使用setAccessible反射(仅限特殊场景)。

❌ 陷阱2:过度Mock

如果测试中Mock了所有类,测试就变成了“测试Mock本身”。建议:只Mock真正外部依赖(IO、第三方库、随机数生成器),自己的逻辑用真的实现。

❌ 陷阱3:忽略with()参数

不写with()时,Mock接受任意参数,但一旦写了with()参数必须完全匹配,建议始终明确参数。

✅ 最佳实践清单

  • 每个测试方法最少断言逻辑,最多一个Mock行为定义
  • 使用@covers注解指明测试的类/方法
  • 对Mock的调用次数要求严格,利于暴露问题
  • setUp()中创建公共Mock,减少重复代码
  • 使用willReturn()而不是willReturnValue()(后者已弃用)

FAQ高频问答

Q1:Mock和Stub到底选哪个? A:如果你只需要“返回预设数据”,用Stub;如果你还需要“验证是否被正确调用”,用Mock,PHPUnit中createMock()生成的是Mock对象,你可以通过expects()让它同时扮演Stub角色——Mock是Stub的超集

Q2:怎么Mock一个静态方法? A:PHPUnit原生无法直接mock静态方法(除非使用uopz扩展或PHPUnit 9+的MockFile),更优雅的方案是:引入门面模式(Facade)或依赖注入容器,将静态调用包裹在非静态方法中。

Q3:Mock对象能测试异常吗? A:可以,用willThrowException()模拟异常,配合expectException()断言异常抛出的类型和消息,这在测试错误处理逻辑时非常有用。

Q4:为什么我的Mock调用没生效? A:最常见原因是——你Mock的是真实类,但真实类中方法被声明为finalprivate,PHPUnit不会警告,但Mock直接返回null,检查方法可见性,确保非final。

Q5:createMockgetMockBuilder何时用谁? A:90%场景用createMock,它默认禁用原构造函数、克隆、自动加载,只有当需要addMethods(给类添加原本不存在的方法)或灵活设置构造函数参数时,才升级到getMockBuilder


从会用走向精通

Mock技术是PHP单元测试的核心武器,掌握了本文的内容,你已经能:

  • ✅ 区分Stub/Mock/Spy的概念
  • ✅ 熟练使用三种Mock创建方式
  • ✅ 用willReturn/willThrowException控制行为
  • ✅ 用约束和调用次数验证互动
  • ✅ 避开常见陷阱,写出干净可靠的测试

Mock不是目的——它只是测试孤独代码的桥梁,如果一段代码需要惊人的Mock配置才能测试,请审视你的设计是否过度耦合。

下一步行动

  1. 在现有项目中对一个Service类写一个Mock测试
  2. 尝试把过度Mock的测试重构为小型真实集成测试(如使用内存数据库)
  3. 阅读PHPUnit官方文档的“Test Doubles”章节

你的单元测试越贴近真实行为,你的代码就越能自然进化,Mock用得多了,你会自然领悟到依赖注入和接口设计本身的意义——测试驱动设计(TDD)的真正魅力


(文章结束)

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