php 8.3 测试只读属性应通过构造函数传参、默认值覆盖或 __unserialize() 重建三种合规方式,禁止反射或运行时修改。

在 PHP 8.3 中,只读属性(readonly)的设计本意就是禁止运行时修改——包括测试中。一旦初始化完成(声明时赋值或构造函数中赋值),任何直接赋值都会触发 Error: Cannot modify readonly property。但测试场景下确实常需“绕过”这一限制,比如模拟不同状态、注入测试数据或验证边界行为。
关键前提是:不能也不应破坏只读语义本身;正确做法是利用语言提供的合法机制,而非强行 hack。
以下是三种安全、合规、推荐用于测试的方案:
✅ 方案一:通过构造函数传入不同初始值(最推荐)
只读属性允许且鼓励在构造函数中初始化。测试时只需构造不同实例即可覆盖各种状态。
class User
{
public function __construct(
public readonly string $name,
public readonly int $age
) {}
}
// 测试不同组合
test('user with adult age', function () {
$user = new User('Alice', 30);
expect($user->age)->toBe(30);
});
test('user with minor age', function () {
$user = new User('Bob', 16); // ✅ 合法:仅在构造时赋值
expect($user->age)->toBe(16);
});
✔️ 优点:完全符合语言规范,零风险,可读性强
❌ 不适用:当构造逻辑复杂、依赖外部服务,或你无法控制构造过程时
✅ 方案二:使用默认值 + 构造参数选择性覆盖(PHP 8.3 新增支持)
PHP 8.3 允许为只读属性设置标量默认值,未传参时自动生效;测试中可灵活选择传或不传。
class Config
{
public function __construct(
public readonly string $host = 'localhost',
public readonly int $port = 8080,
public readonly bool $debug = false,
) {}
}
// 测试默认行为
test('uses default host and port', function () {
$config = new Config();
expect($config->host)->toBe('localhost');
expect($config->port)->toBe(8080);
});
// 测试自定义值
test('overrides debug flag', function () {
$config = new Config(debug: true); // ✅ PHP 8.3 支持命名参数
expect($config->debug)->toBe(true);
});
✔️ 优点:减少构造冗余,适合配置类、DTO 等场景
⚠️ 注意:默认值仅支持标量、null、字面量数组(PHP 8.3.2+)、类常量,不支持函数调用或表达式
✅ 方案三:在 __unserialize() 中重建只读状态(仅限序列化/反序列化测试)
如果你的测试涉及 unserialize() 场景(如缓存、会话、持久化恢复),PHP 8.3 明确允许在 __unserialize() 方法内首次写入只读属性(前提是它尚未被初始化)。
class Payload
{
public readonly string $id;
public readonly array $data;
public function __construct(string $id, array $data)
{
$this->id = $id;
$this->data = $data;
}
public function __unserialize(array $data): void
{
// ✅ 合法:只读属性尚未初始化,可在 __unserialize() 中赋值
$this->id = $data['id'] ?? 'fallback';
$this->data = $data['data'] ?? [];
}
}
// 测试反序列化逻辑
test('restores readonly properties via __unserialize', function () {
$payload = new Payload('abc', ['x' => 1]);
$serialized = serialize($payload);
// 模拟反序列化流程
$restored = @unserialize($serialized);
expect($restored->id)->toBe('abc');
});
✔️ 优点:符合 PHP 8.3 官方反序列化契约,适用于框架/ORM 底层测试
❗ 前提:必须确保该只读属性在反序列化前确实未被初始化(即不能在__construct中已赋值,否则会报错)
不推荐的做法(请避免):
- 尝试用
ReflectionProperty::setValue()强制修改 → PHP 8.3 会抛出Error(反射不再绕过只读检查) - 使用
eval()、runkit或其他扩展篡改内存 → 破坏类型安全,测试失真,且 PHP 8.3 已禁用此类行为 - 把
readonly改成普通属性再测 → 失去测试意义,掩盖真实约束
本质上,只读属性不是“难以测试”,而是要求你以更清晰、更不可变的方式设计对象。测试的重点应转向:
→ 构造是否正确初始化了只读字段
→ 只读字段是否被意外修改(可用错误断言捕获)
→ 序列化/反序列化是否保持一致性
这样写出的测试,才是真正健壮的。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











