Symfony3.4单元测试Mock服务异常:断言信息混入响应输出
背景代码
控制器代码
我有一个以服务形式存在的DefaultController,依赖注入为自动注入模式,代码如下:
class DefaultController extends BaseController { private $logger; private $recordRepository; public function __construct(LoggerInterface $logger, RecordRepository $recordRepository) { $this->logger = $logger; $this->recordRepository = $recordRepository; } public function downloadAction($uuid) { $recordRepo = $this->recordRepository; $record = $recordRepo->findActive($uuid); $logger = $this->logger; if (!$record) { $logger->error('Record not found: '.$uuid); return $this->render("default/download_error.html.twig", [ 'error' => 'record_not_found' ]); } } }
单元测试代码
我编写了一个单元测试,尝试调用该方法并将Logger服务替换为Mock对象,测试代码如下:
<?php namespace Tests\AppBundle\Controller; use Monolog\Logger; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class DefaultControllerTest extends WebTestCase { public function testDownloadAction() { $mock = $this->getMockBuilder(Logger::class) ->disableOriginalConstructor() ->getMock(); $mock->expects($this->once())->method('error') ->with('Record not found: 1223'); $client = static::createClient([]); $client->getContainer()->set('logger', $mock); $client->request('GET', '/download/12233'); print $client->getResponse()->getContent(); $this->assertEquals(200, $client->getResponse()->getStatusCode()); } }
遇到的问题
当测试失败(Logger的error方法调用参数与预期不匹配)时,PHPUnit的错误信息被放入$client->getResponse的输出中,而非在CLI中直接显示。CLI输出只显示错误信息以500页面形式返回,最终PHPUnit仅断言响应状态码为200失败,看不到真正的Mock验证失败原因。请问我是否需要使用普通PHPUnit测试用例来使用Mock对象?如果是,是否需要重构控制器中的$this->render方法?
解决方案分析
为什么会出现这个问题
你用的WebTestCase会启动Symfony内核,而内核默认会捕获所有异常并转换成HTTP 500响应页面。当你的Mock Logger验证失败时,PHPUnit抛出的ExpectationFailedException被内核捕获,变成了500错误页面,导致你在CLI里看不到真正的失败原因,只能看到状态码断言失败。
方案1:改用普通PHPUnit测试(推荐)
不需要依赖WebTestCase,直接实例化控制器并手动注入Mock依赖,这样测试是直接调用代码逻辑,不走HTTP请求流程,异常会直接在CLI抛出,能看到完整的Mock验证失败信息。
示例代码如下:
<?php namespace Tests\AppBundle\Controller; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use AppBundle\Controller\DefaultController; use AppBundle\Repository\RecordRepository; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Response; use Twig\Environment; class DefaultControllerTest extends TestCase { public function testDownloadActionWhenRecordNotFound(): void { // 1. Mock Logger,设置预期调用 /** @var MockObject&LoggerInterface $loggerMock */ $loggerMock = $this->createMock(LoggerInterface::class); $loggerMock->expects($this->once()) ->method('error') ->with('Record not found: 1223'); // 2. Mock RecordRepository,让它返回null模拟记录不存在 /** @var MockObject&RecordRepository $repoMock */ $repoMock = $this->createMock(RecordRepository::class); $repoMock->method('findActive') ->with('1223') ->willReturn(null); // 3. Mock Twig环境,因为控制器要调用render方法 /** @var MockObject&Environment $twigMock */ $twigMock = $this->createMock(Environment::class); $twigMock->method('render') ->with('default/download_error.html.twig', ['error' => 'record_not_found']) ->willReturn('<html>Record not found error</html>'); // 4. 实例化控制器并注入所有Mock依赖 $controller = new DefaultController($loggerMock, $repoMock); // 给控制器设置Twig(根据你的BaseController实现调整,比如如果有$twig属性或者setter) $controller->twig = $twigMock; // 5. 直接调用action方法 $response = $controller->downloadAction('1223'); // 6. 断言响应结果 $this->assertEquals(200, $response->getStatusCode()); $this->assertStringContainsString('record_not_found', $response->getContent()); } }
方案2:修改WebTestCase配置,让内核不捕获异常
如果你还是想保留HTTP请求式的测试,可以让Symfony内核在测试环境下不捕获异常,这样PHPUnit的Mock验证异常会直接抛出到CLI。
方法A:创建客户端时临时设置
$client = static::createClient([], [ 'error_handler' => [ 'throw_exception' => true, ], ]);
方法B:全局配置测试环境
在config/packages/test/framework.yaml中添加:
framework: error_handler: throw_exception: true
这样所有测试用例都会直接抛出异常,而不是转换成500页面。不过这种方式可能会让一些HTTP层面的错误也直接抛出,需要根据你的测试需求权衡。
关于是否需要重构render方法
不需要强制重构!只要你能正确Mock控制器依赖的Twig环境,就可以正常测试。当然,如果后续想让控制器更易测试,可以考虑把响应生成逻辑(比如渲染模板)抽象成独立的服务,但当前场景下完全不需要大改。
内容的提问来源于stack exchange,提问作者Oli

