如何测试PHP被测代码对Trait方法的调用且不额外添加生产代码?
测试使用Trait的类的方法调用(无需额外生产代码)
你核心的诉求是:验证ClassThatWillLog::foobar()是否正确调用了CanLog里的log方法,同时不想给Trait本身加测试相关的生产代码,也不想只能靠集成测试验证。下面是几个可行的方案:
方案1:在测试类中重写Trait方法
利用PHP的特性:类中定义的方法优先级高于Trait中的同名方法。你可以在测试时创建ClassThatWillLog的子类,重写log方法来收集日志,然后断言内容。
// 测试代码 class ClassThatWillLogTest extends PHPUnit\Framework\TestCase { public function testFoobarLogsSuccessMessage() { // 定义测试用的子类,重写log方法收集日志 $testClass = new class extends ClassThatWillLog { public array $loggedMessages = []; protected function log($level, $message): void { $this->loggedMessages[] = ['level' => $level, 'message' => $message]; } }; // 执行被测方法 $testClass->foobar(); // 断言日志内容 $this->assertEquals('INFO', $testClass->loggedMessages[0]['level']); $this->assertStringContainsString('I finished successfully', $testClass->loggedMessages[0]['message']); } }
这个方案完全不需要修改生产代码,只在测试中临时扩展类即可,简单直接。
方案2:用PHPUnit的Mock工具替换Trait方法
如果用PHPUnit,可以通过部分Mock的方式,直接替换目标类中的log方法,记录它的调用参数:
// 测试代码 class ClassThatWillLogTest extends PHPUnit\Framework\TestCase { public function testFoobarLogsSuccessMessage() { // 创建部分Mock,只替换log方法 $mock = $this->getMockBuilder(ClassThatWillLog::class) ->onlyMethods(['log']) // 指定要替换的方法(即使是protected) ->getMock(); // 预期log方法会被调用一次,参数为INFO和指定消息 $mock->expects($this->once()) ->method('log') ->with( $this->equalTo('INFO'), $this->stringContains('I finished successfully') ); // 执行被测方法 $mock->foobar(); } }
注意:PHPUnit可以处理protected方法的Mock,只要你在onlyMethods中指定它即可,这个方案适合需要验证调用次数、参数的场景。
方案3:重构Trait兼顾组合与可测试性
如果你希望Trait保留组合特性,同时拥有类似DI的可测试性,可以让Trait依赖一个日志接口,而不是自己实现日志逻辑:
// 生产代码(仅重构Trait,不破坏原有用法) trait CanLog { protected LoggerInterface $logger; // 提供一个设置logger的方法,或者在Trait中默认初始化(兼容原有代码) public function setLogger(LoggerInterface $logger): void { $this->logger = $logger; } protected function log($level, $message): void { // 延迟初始化,兼容原有未注入的场景 if (!isset($this->logger)) { $this->logger = new DefaultLogger(); // 你的实际日志实现 } $this->logger->log($level, $message); } } // 测试时注入FakeLogger class ClassThatWillLogTest extends PHPUnit\Framework\TestCase { public function testFoobarLogsSuccessMessage() { $fakeLogger = new FakeLogger(); $testClass = new ClassThatWillLog(); $testClass->setLogger($fakeLogger); $testClass->foobar(); $logged = $fakeLogger->getLogMessages()[0]; $this->assertStringStartsWith('INFO: I finished successfully', $logged); } }
这个方案的好处是既保留了Trait的组合优势,又让代码具备了DI的可测试性,而且对原有代码的侵入性极低——如果原有类没有设置logger,Trait会自动用默认实现,完全不影响生产环境的使用。
内容的提问来源于stack exchange,提问作者jrgilman
相关产品推荐
相关产品推荐

