
本文讲解在 PHPUnit 单元测试中,当被测类依赖已单独测试过的类(如 SomePerson 依赖 SomeCalculator)时,应通过依赖注入 + Mock 隔离外部行为,仅验证自身逻辑,避免重复断言和测试脆弱性。
本文讲解在 phpunit 单元测试中,当被测类依赖已单独测试过的类(如 `someperson` 依赖 `somecalculator`)时,应通过依赖注入 + mock 隔离外部行为,仅验证自身逻辑,避免重复断言和测试脆弱性。
在单元测试实践中,一个核心原则是:每个测试只负责验证当前被测单元(SUT, System Under Test)的行为,不承担验证其依赖组件正确性的责任。若 SomePerson 的测试中再次断言 add(1, 1) === 2,实质上是在重复 SomeCalculatorTest 的职责——这不仅造成冗余,更会使 SomePersonTest 因 SomeCalculator 内部实现变更而意外失败,违背单元测试的“隔离性”与“稳定性”。
问题根源在于原始代码中的硬编码依赖:
$this->age = (new SomeCalculator())->add($a, $b); // ❌ 紧耦合,无法替换/模拟
此类设计导致 SomePerson 与 SomeCalculator 在构造时强绑定,既难以测试,也违反 SOLID 中的依赖倒置原则(DIP)。
✅ 正确解法:职责分离 + 依赖注入 + Mock 验证
我们应将“数据建模”与“对象创建逻辑”解耦:
立即学习“PHP免费学习笔记(深入)”;
- SomePerson 降级为纯粹的数据载体(类似 DTO),仅接收并存储 name 和 age;
- 新增 PersonFactory 负责组合逻辑:接收 SomeCalculator 实例,调用其方法,并构造 SomePerson。
重构后代码如下:
class SomePerson
{
protected string $name;
protected int $age;
public function __construct(string $name, int $age)
{
$this->name = $name;
$this->age = $age;
}
public function getName(): string { return $this->name; }
public function getAge(): int { return $this->age; }
}
class PersonFactory
{
private SomeCalculator $calculator;
public function __construct(SomeCalculator $calculator)
{
$this->calculator = $calculator;
}
public function create(string $name, int $a, int $b): SomePerson
{
$age = $this->calculator->add($a, $b);
return new SomePerson($name, $age);
}
}? 对应的 PHPUnit 测试(使用 Mock)
在 PersonFactoryTest 中,我们不关心 SomeCalculator::add() 是否真的返回 2,只验证:
- 工厂是否以正确的参数($a, $b)调用了 add() 方法;
- 是否将该调用结果准确传递给了 SomePerson 构造函数。
use PHPUnit\Framework\TestCase;
class PersonFactoryTest extends TestCase
{
public function testCreate(): void
{
// Arrange: Mock dependency, stub expected behavior
$calculator = $this->createMock(SomeCalculator::class);
$calculator->expects($this->once())
->method('add')
->with(1, 1)
->willReturn(2);
$factory = new PersonFactory($calculator);
// Act
$person = $factory->create('Horst', 1, 1);
// Assert: Only verify SUT's own contract
$this->assertSame('Horst', $person->getName());
$this->assertSame(2, $person->getAge());
}
}⚠️ 注意事项:
- expects($this->once()) 明确声明了对 add() 的调用次数预期,增强测试可读性与健壮性;
- Mock 返回值可任意设定(如 999),只要与断言一致即可——因为重点是交互协议,而非被依赖类的真实计算逻辑;
- 若未来 SomeCalculator 改为浮点运算或引入缓存,PersonFactoryTest 无需修改,真正实现“解耦测试”。
✅ 总结:单元测试的黄金准则
| 原则 | 实践方式 |
|---|---|
| 单一职责 | 每个测试类只验证一个类;每个测试方法只聚焦一个行为路径 |
| 依赖隔离 | 用 Mock/Stub 替代真实依赖,切断外部副作用影响 |
| 行为验证 > 状态验证 | 优先 expects()->method()->with()->willReturn(),而非断言最终状态是否“恰好等于某值”(除非该状态是 SUT 的核心契约) |
| 重构友好 | 当底层依赖逻辑变更时,上层测试不应连锁失败——这是良好架构与测试设计的直接体现 |
遵循此模式,你的测试套件将更轻量、更稳定、更具可维护性,真正成为驱动高质量 PHP 应用开发的可靠基石。



















