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

API端点测试中Mock NewsletterService未生效问题排查

问题:Symfony功能测试中Mock服务未注入导致验证失败

栈版本

  • ApiPlatform 3.2
  • Symfony 6.4
  • PhpUnit 9.5
  • PHP 8.0

问题背景

我正在为User资源的DELETE端点编写功能测试,该端点需实现用户软删除、清空/覆盖敏感字段,且用户需满足特定条件才可删除。通过响应数据或数据库查询可验证端点功能,但还需验证NewsletterService::removeUserFromAlLists()方法是否被调用。

我创建了NewsletterService的Mock对象并替换测试容器中的对应服务,但执行请求时,DeleteUserAction注入的仍是真实的NewsletterService,导致测试失败。

失败信息

There was 1 failure:

1) App\Tests\Functional\Entity\UserTest::testErase()
Expectation failed for method name is "removeUserFromAlLists" when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.

测试代码

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use App\Entity\Enum\User\UserErasedReasonsEnum;
use App\Entity\User;
use App\Factory\UserFactory;
use App\Service\NewsletterService;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Zenstruck\Browser\HttpOptions;
use Zenstruck\Browser\Test\HasBrowser;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Test\ResetDatabase;

class UserTest extends ApiTestCase
{
    use ResetDatabase;
    use Factories;
    use HasBrowser;

    public function testErase(): void
    {
        static::bootKernel();
        $container = static::getContainer();

        $userToAuthenticate = UserFactory::createOne();
        $user = UserFactory::createOne([]);

        $newsletterServiceMock = $this->createMock(NewsletterService::class);
        $newsletterServiceMock->expects($this->once())
            ->method('removeUserFromAlLists')
            ->with('email', $user->getEmail());
        $container->set(NewsletterService::class, $newsletterServiceMock);

        $erasedDate = (new \DateTime())->setTime(0, 0);
        $erasedReason = (string)UserErasedReasonsEnum::WHATEVER;

        $this->browser()
            ->disableReboot()
            ->actingAs($userToAuthenticate->_real())
            ->delete($this->getIriFromResource($user))
            ->assertSuccessful()
        ;
    }

}

DeleteUserAction代码

use App\Service\NewsletterService;
use Symfony\Bundle\SecurityBundle\Security;
use PHPUnit\Framework\MockObject\MockObject;

class DeleteAction
{
    public function __construct(
        private readonly Security $security,
        private readonly NewsletterService $newsletterService,
    ) {}

    public function __invoke(Request $request, int $id): Response
    {
        $user = $this->security->getUser();

        $this->newsletterService->removeUserFromAlLists($user->getEmail());
        // dd($this->newsletterService instanceof MockObject ? 'true' : 'false'); 
        // 取消注释后返回false
        
        // ...
    }
}

问题原因与解决方案

核心原因

  1. 服务实例化顺序问题:Symfony容器在Kernel启动后,可能已提前实例化了DeleteUserAction(比如预加载逻辑),此时它的构造函数已注入真实的NewsletterService实例。后续替换容器中的服务,不会更新已实例化类的依赖引用。
  2. Mock参数不匹配:测试中Mock期望接收两个参数('email'和被删除用户的邮箱),但实际DeleteUserAction只传递了当前认证用户的邮箱这一个参数——即使Mock被正确注入,这个断言也会失败。

修复步骤

1. 修正Mock的参数匹配逻辑

根据DeleteUserAction的实际调用,调整Mock的参数期望:

// 原错误代码
$newsletterServiceMock->expects($this->once())
    ->method('removeUserFromAlLists')
    ->with('email', $user->getEmail());

// 修正后
$newsletterServiceMock->expects($this->once())
    ->method('removeUserFromAlLists')
    ->with($userToAuthenticate->getEmail());

因为DeleteUserAction中$user = $this->security->getUser()获取的是执行删除操作的认证用户,而非被删除的用户。

2. 确保Mock在服务实例化前注入

修改测试代码,在设置Mock前清空已初始化的服务实例,确保后续请求使用Mock:

public function testErase(): void
{
    $userToAuthenticate = UserFactory::createOne();
    $user = UserFactory::createOne([]);

    // 先创建Mock
    $newsletterServiceMock = $this->createMock(NewsletterService::class);
    $newsletterServiceMock->expects($this->once())
        ->method('removeUserFromAlLists')
        ->with($userToAuthenticate->getEmail());

    // 启动Kernel并获取容器
    static::bootKernel();
    $container = static::getContainer();

    // 如果服务已被实例化,先清空现有实例
    if ($container->initialized(NewsletterService::class)) {
        $container->set(NewsletterService::class, null);
    }
    // 设置Mock到容器
    $container->set(NewsletterService::class, $newsletterServiceMock);

    $this->browser()
        ->disableReboot()
        ->actingAs($userToAuthenticate->_real())
        ->delete($this->getIriFromResource($user))
        ->assertSuccessful()
    ;
}

备选方案:在Kernel启动阶段注入Mock

如果上述方法仍不生效,可以在Kernel启动时直接注入Mock,确保服务定义在容器构建阶段就被替换:

public function testErase(): void
{
    $userToAuthenticate = UserFactory::createOne();
    $user = UserFactory::createOne([]);

    $newsletterServiceMock = $this->createMock(NewsletterService::class);
    $newsletterServiceMock->expects($this->once())
        ->method('removeUserFromAlLists')
        ->with($userToAuthenticate->getEmail());

    // 启动Kernel时注入Mock
    static::bootKernel(function (\Symfony\Component\HttpKernel\KernelInterface $kernel) use ($newsletterServiceMock) {
        $kernel->getContainer()->set(NewsletterService::class, $newsletterServiceMock);
    });

    $this->browser()
        ->disableReboot()
        ->actingAs($userToAuthenticate->_real())
        ->delete($this->getIriFromResource($user))
        ->assertSuccessful()
    ;
}

内容的提问来源于stack exchange,提问作者fombo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:44:55