如何测试会创建继承类实例的静态方法?及代码优化建议
如何测试会创建继承类实例的静态方法?及代码优化建议
嘿,我来帮你梳理下这个问题——你遇到的核心困境其实是静态方法+硬编码类实例化导致的测试死局:你在sendMessagesForDay里直接new $messageType(...),自己mock的对象根本没被用到,自然验证不了triggerMail的调用。下面分两部分给你解决思路:
一、临时测试方案(不改动现有代码)
如果暂时不想大改代码,你可以用Mockery的类重载特性拦截ItemMessage的实例化,让方法里new出来的对象就是你mock的实例:
<?php namespace Tests\Service; use App\Messages\ItemMessage; use App\Service\MassMessenger; use App\Settings\Settings; use PHPUnit\Framework\TestCase; use Mockery; class MassMessengerTest extends TestCase { public function testSendItem() { $day = 1; // Mock Settings对象 $settings = $this->createMock(Settings::class); $settings->method('getEmailContent')->willReturn('Test Content'); $settings->method('getEmailTitle')->willReturn('Test Title'); // 创建ItemMessage的mock,验证triggerMail调用 $messageMock = Mockery::mock(ItemMessage::class); $messageMock->shouldReceive('triggerMail')->once(); // 重载ItemMessage类,让new的时候返回我们的mock Mockery::mock('overload:' . ItemMessage::class) ->shouldReceive('__construct') ->withAnyArgs() ->andReturn($messageMock); MassMessenger::sendItem($day, $settings); } protected function tearDown(): void { parent::tearDown(); Mockery::close(); } }
不过这个方案是“曲线救国”,本质上依赖Mockery的黑魔法,不是长久之计,最好还是从代码设计上根治问题。
二、代码优化建议(从根源提升可测试性)
你的代码“异味”主要来自静态方法滥用和硬编码实例化,这两个点把代码和具体实现死死绑定,完全失去了灵活性。这里给你几个重构方向:
1. 去掉静态方法,改成实例类+依赖注入
静态方法最大的问题就是无法依赖注入,也没法被替换/继承。我们把MassMessenger改成普通类,同时引入消息工厂来创建ItemMessage和LocationMessage实例,不再直接new:
namespace App\Service; use App\Messages\ItemMessage; use App\Messages\LocationMessage; use App\Messages\Message; use App\Settings\Settings; // 定义消息工厂接口(遵循依赖倒置原则) interface MessageFactoryInterface { public function createItem(int $postId, string $content, string $title): Message; public function createLocation(int $postId, string $content, string $title): Message; } // 实现消息工厂 class MessageFactory implements MessageFactoryInterface { public function createItem(int $postId, string $content, string $title): Message { return new ItemMessage($postId, $content, $title); } public function createLocation(int $postId, string $content, string $title): Message { return new LocationMessage($postId, $content, $title); } } // 重构MassMessenger为实例类 class MassMessenger { private $messageFactory; // 构造函数注入工厂 public function __construct(MessageFactoryInterface $messageFactory) { $this->messageFactory = $messageFactory; } protected function sendMessagesForDay(int $day, string $content, string $title, callable $messageCreator): void { $posts = \App\Repository\Post::getPostForDay($day); foreach ($posts as $post) { // 通过回调创建消息实例,避免硬编码 $message = $messageCreator($post->getId(), $content, $title); $message->triggerMail(); } } public function sendItem(int $day, Settings $settings): void { $this->sendMessagesForDay( $day, $settings->getEmailContent(), $settings->getEmailTitle(), fn($id, $content, $title) => $this->messageFactory->createItem($id, $content, $title) ); } public function sendLocation(int $day, Settings $settings): void { $this->sendMessagesForDay( $day, $settings->getEmailContent(), $settings->getEmailTitle(), fn($id, $content, $title) => $this->messageFactory->createLocation($id, $content, $title) ); } }
2. 重构后的测试会变得非常简单
现在你可以轻松mock工厂和消息对象,完全隔离外部依赖:
<?php namespace Tests\Service; use App\Messages\ItemMessage; use App\Service\MassMessenger; use App\Service\MessageFactoryInterface; use App\Settings\Settings; use PHPUnit\Framework\TestCase; use Mockery; class MassMessengerTest extends TestCase { public function testSendItem() { $day = 1; // Mock Settings $settings = $this->createMock(Settings::class); $settings->method('getEmailContent')->willReturn('Test Content'); $settings->method('getEmailTitle')->willReturn('Test Title'); // Mock ItemMessage,验证triggerMail调用 $itemMessageMock = Mockery::mock(ItemMessage::class); $itemMessageMock->shouldReceive('triggerMail')->once(); // Mock消息工厂,指定返回我们的mock对象 $factoryMock = Mockery::mock(MessageFactoryInterface::class); $factoryMock->shouldReceive('createItem') ->with(1, 'Test Content', 'Test Title') // 假设测试时Post仓库返回id=1的帖子 ->andReturn($itemMessageMock); // 注入mock工厂,实例化MassMessenger $massMessenger = new MassMessenger($factoryMock); $massMessenger->sendItem($day, $settings); } protected function tearDown(): void { parent::tearDown(); Mockery::close(); } }
3. 额外优化点
- 如果
Post::getPostForDay也是硬编码的,建议把PostRepository也做成依赖注入,测试时可以mock仓库返回固定数据,彻底隔离数据库; - 把
triggerMail的逻辑抽象成MailerInterface,这样连邮件发送逻辑都能轻松测试,不用依赖PHPMailer的实际调用; - 静态方法尽量少用,除非是纯工具类(无状态、不依赖外部资源),否则都会严重影响代码的可测试性和扩展性。
备注:内容来源于stack exchange,提问作者Fliwatt
相关产品推荐
相关产品推荐

