
PHP 8.1 引入 public readonly 属性后,传统基于方法的 mock 策略失效;本文详解通过 PHPUnit MockBuilder 启用构造函数初始化、直接实例化 DTO 或重构设计等三种可落地的测试方案。
php 8.1 引入 `public readonly` 属性后,传统基于方法的 mock 策略失效;本文详解通过 phpunit mockbuilder 启用构造函数初始化、直接实例化 dto 或重构设计等三种可落地的测试方案。
当 PHP 升级至 8.1+,使用 public readonly 替代 getter 方法(如 getValueOne())虽显著减少样板代码,却给单元测试带来新挑战:PHPUnit 默认 createMock() 会禁用原始构造函数(disableOriginalConstructor()),导致 readonly 属性未被初始化,始终为 null;而尝试直接赋值($mock->valueOne = $mockObj)则触发致命错误 Cannot initialize readonly property ... from scope ...。
✅ 正确方案一:使用 MockBuilder 启用构造函数(推荐)
核心思路是让 mock 实例真正执行 DataTransferObject 的构造函数,从而完成 public readonly 属性的初始化:
use PHPUnit\Framework\TestCase;
class ConsumerTest extends TestCase
{
public function testNameIsCorrect(): void
{
// 1. 创建 ValueObjectOne 的 mock
$valueOneMock = $this->createMock(ValueObjectOne::class);
$valueOneMock->expects($this->once())->method('isValid')->willReturn(true);
// 2. 使用 MockBuilder 显式启用构造函数,并传入依赖
$dtoMock = $this->getMockBuilder(DataTransferObject::class)
->disableOriginalClone() // 可选:防止意外克隆
->enableOriginalConstructor() // ✅ 关键:启用构造函数
->setConstructorArgs([$valueOneMock, new ValueObjectTwo()]) // 提供所有 readonly 参数
->getMock();
// 3. 构造 Consumer(此时 $dto->valueOne 已正确初始化)
$consumer = new Consumer($dtoMock);
// 4. 断言
$this->assertSame('Adam', $consumer->getName());
}
}
⚠️ 注意:setConstructorArgs() 必须按 DataTransferObject::__construct() 参数顺序传入——ValueObjectOne 和 ValueObjectTwo 缺一不可。若 ValueObjectTwo 在测试中无需行为验证,可直接传入新实例(如 new ValueObjectTwo())或使用 createStub()。
✅ 方案二:不 mock,直接实例化 DTO(更简洁、更真实)
DTO(数据传输对象)本质是不可变的数据容器,无业务逻辑、无副作用。与其费力 mock,不如直接创建真实实例:
public function testNameIsCorrect(): void
{
// 直接构造真实 DTO:注入已配置好的 ValueObjectOne
$valueOne = $this->createMock(ValueObjectOne::class);
$valueOne->expects($this->once())->method('isValid')->willReturn(true);
$dto = new DataTransferObject($valueOne, new ValueObjectTwo()); // ✅ 简洁、可靠、零 mock 开销
$consumer = new Consumer($dto);
$this->assertSame('Adam', $consumer->getName());
}
该方式规避了 mock 的复杂性,提升测试可读性与执行速度,且更贴近生产环境行为——只要 DTO 不含外部依赖(如数据库、HTTP 客户端),它就是理想的“可直接实例化”对象。
❌ 不推荐方案:强行绕过 readonly 限制
试图通过反射、__set() 或其他 hack 手段修改 readonly 属性不仅违反语言设计原则,还会导致测试脆弱(PHP 版本升级可能彻底禁止)、难以维护,应坚决避免。
总结与建议
| 方案 | 适用场景 | 优势 | 风险 |
|---|---|---|---|
| MockBuilder + enableOriginalConstructor | DTO 依赖复杂(如需 mock 多个深层对象)或必须隔离外部行为时 | 严格控制依赖,符合 mock 原则 | 配置稍繁琐,需确保参数完整 |
| 直接 new DTO | DTO 纯数据、依赖简单(如仅 ValueObject) | 最简、最快、最可靠、易读 | 若 DTO 构造器含副作用(如远程调用),则不可用 |
| 重构为 protected + getter(降级) | 临时兼容旧测试框架 | 无需改测试逻辑 | 放弃 readonly 优势,属倒退 |
✅ 最佳实践推荐:优先采用「直接实例化 DTO」;仅当 DTO 构造成本极高或存在不可控副作用时,再选用 MockBuilder 方案。同时,借此机会审视:是否所有 DTO 都真正需要被 mock?减少不必要的 mock,往往能写出更健壮、更可持续的测试套件。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











