
本文讲解如何通过依赖注入、工厂模式和 mock 对象,使被测类解耦于其依赖项,从而在单元测试中专注验证自身行为,避免对已单独测试过的类重复断言。
本文讲解如何通过依赖注入、工厂模式和 mock 对象,使被测类解耦于其依赖项,从而在单元测试中专注验证自身行为,避免对已单独测试过的类重复断言。
在 PHPUnit 单元测试实践中,一个常见误区是:当类 A 依赖类 B(且类 B 已有完备测试),开发者仍倾向于在测试类 A 时再次断言类 B 的具体行为(如 SomePersonTest 中校验 getAge() === 2)。这不仅造成逻辑冗余,更暴露了设计缺陷——紧耦合。
以原始代码为例:
class SomePerson {
public function __construct($name, $a, $b) {
$this->age = (new SomeCalculator())->add($a, $b); // ❌ 硬编码实例化,无法替换依赖
}
}
该写法导致 SomePerson 与 SomeCalculator 强绑定,测试时无法隔离验证:你既在 SomeCalculatorTest 中保证了 add(1, 1) === 2,又在 SomePersonTest 中重复断言 getAge() === 2,实则是在为别人的逻辑“背书”,违背单元测试“只测被测单元自身职责”的核心原则。
✅ 正确做法是应用 依赖倒置原则(DIP) 和 控制反转(IoC):
- 剥离构造逻辑:将 SomePerson 改为纯数据载体(DTO 风格),仅接收计算结果,不参与运算;
- 引入工厂层:由 PersonFactory 负责协调 SomeCalculator 与 SomePerson,并显式声明依赖;
- 使用 Mock 隔离外部行为:在测试工厂时,Mock SomeCalculator,仅验证参数传递与结果使用是否正确,而非计算逻辑本身。
重构后代码如下:
class SomePerson {
private string $name;
private 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);
}
}
对应测试应聚焦工厂逻辑,而非计算器实现:
class PersonFactoryTest extends TestCase {
public function testCreate() {
// Arrange
$mockCalculator = $this->createMock(SomeCalculator::class);
$mockCalculator->method('add')
->with(1, 1)
->willReturn(2); // 模拟返回值,不关心 add 如何实现
$factory = new PersonFactory($mockCalculator);
// Act
$person = $factory->create('Horst', 1, 1);
// Assert:只验证工厂是否正确使用了依赖结果
$this->assertSame('Horst', $person->getName());
$this->assertSame(2, $person->getAge());
}
}
⚠️ 注意事项:
- 不要在 SomePersonTest 中断言 getAge() 的数值逻辑——那属于 SomeCalculator 的契约,应由其专属测试保障;
- Mock 的返回值可任意设定(如 willReturn(999)),只要它能验证“工厂是否按预期调用并使用结果”即可;过度模拟真实值反而降低测试鲁棒性;
- 若 SomePerson 本身需业务逻辑(如年龄校验),应在 SomePersonTest 中针对该逻辑编写断言,而非复现 SomeCalculator 的数学断言。
总结:单元测试的价值在于快速、精准地定位故障模块。通过解耦依赖、明确职责边界,并借助 Mock 控制协作行为,你能写出真正独立、可维护、高信噪比的测试套件——让每个测试只回答一个问题:“这个类,在给定条件下,是否按预期工作?”
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











