You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试会创建继承类实例的静态方法?及代码优化建议

如何测试会创建继承类实例的静态方法?及代码优化建议

嘿,我来帮你梳理下这个问题——你遇到的核心困境其实是静态方法+硬编码类实例化导致的测试死局:你在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.17 11:50:30