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 // ... } }
问题原因与解决方案
核心原因
- 服务实例化顺序问题:Symfony容器在Kernel启动后,可能已提前实例化了
DeleteUserAction(比如预加载逻辑),此时它的构造函数已注入真实的NewsletterService实例。后续替换容器中的服务,不会更新已实例化类的依赖引用。 - 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
相关产品推荐
相关产品推荐

