Symfony测试中如何模拟TransportException触发catch代码块
问题背景
我正在为一个小型Symfony 6项目编写测试用例。项目中包含一个用于表示Web表单请求的自定义类,该类实现了多个借助Symfony MailerInterface发送各类邮件的方法,核心方法代码如下:
public function sendConfirmationEmail(): array { if (!$this->validated) { return [ 'success' => false, 'message' => 'Inquiry has not been validated. Use Inquiry->validate() first', ]; } if (!$this->email) { return [ 'success' => false, 'message' => 'Benutzer hat keine Emailadresse angegeben' ]; } $email = (new TemplatedEmail()) ->to($this->email) ->subject('Eingangsbestätigung') ->htmlTemplate('confirmationEmail.html.twig') ->textTemplate('confirmationEmail.txt.twig') ->context([ 'name' => $this->name, 'mail' => $this->email, 'phone' => $this->phone, 'subject' => $this->subject, 'message' => $this->message, ]); try { $this->mailer->send($email); $this->logger->debug('Confirmation mail sent'); return [ 'success' => true, 'message' => 'Email wurde gesendet', ]; } catch (TransportExceptionInterface $e) { $this->logger->debug('Error sending confirmation email: ' . $e); return [ 'success' => false, 'message' => 'Email konnte nicht gesendet werden: ' . $e, ]; } }
mailer服务通过类构造函数以只读属性方式注入,构造函数代码如下:
public function __construct( private readonly LoggerInterface $logger, private readonly MailerInterface $mailer, private readonly array $officeRecipients, private readonly ValidatorInterface $validator, ) { }
目前使用Symfony的KernelTestCase测试邮件发送成功的场景没有问题,但需要在单元测试中覆盖该方法的catch分支逻辑。
待解决问题:如何在测试过程中模拟
TransportException异常,触发对应的catch代码块?
实现方案
这种依赖全通过构造函数注入的类,不需要依赖KernelTestCase做集成测试,直接用PHPUnit原生的Mock对象模拟依赖抛异常即可,测试执行速度更快,逻辑也更可控。
核心思路
- 给
MailerInterface创建测试模拟桩,配置其send()方法被调用时,直接抛出实现了TransportExceptionInterface的异常实例,不要抛普通的Exception,否则catch块无法捕获。可以直接用Symfony自带的Symfony\Component\Mailer\Exception\TransportException类,它已经实现了对应接口,不需要自己编写异常模拟类。 - 其余依赖(Logger、Validator、收件人配置)根据测试场景创建模拟对象或传入合法测试值即可,不需要拉起整个Symfony服务容器。
- 执行测试方法前先给待测试类填充合法属性,跳过前面的参数校验分支,保证代码能走到
$this->mailer->send()的逻辑位置。 - 执行方法后断言返回值符合catch分支预期,同时可以校验日志是否被正确写入。
测试代码示例
<?php namespace App\Tests\Unit; use App\Inquiry; // 替换为你自己的表单类实际命名空间 use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; class InquiryTest extends TestCase { private MockObject|LoggerInterface $logger; private MockObject|MailerInterface $mailer; private MockObject|ValidatorInterface $validator; private array $testOfficeRecipients = ['office@example.com']; protected function setUp(): void { // 初始化所有依赖的模拟对象 $this->logger = $this->createMock(LoggerInterface::class); $this->mailer = $this->createMock(MailerInterface::class); $this->validator = $this->createMock(ValidatorInterface::class); } public function testSendConfirmationEmailWhenTransportFailed(): void { // 配置mailer模拟:调用send时直接抛出传输异常 $testException = new TransportException('SMTP server connection timeout'); $this->mailer->expects($this->once()) ->method('send') ->willThrowException($testException); // 断言logger确实记录了错误日志 $this->logger->expects($this->once()) ->method('debug') ->with($this->stringContains('Error sending confirmation email:')); // 初始化待测试类实例 $inquiry = new Inquiry( $this->logger, $this->mailer, $this->testOfficeRecipients, $this->validator ); // 填充合法属性,跳过前置校验逻辑 $inquiry->validate(); // 如果你的validate方法依赖validator返回结果,提前配置validator的模拟返回值即可 $inquiry->email = 'user@test.com'; $inquiry->name = 'Test User'; // 按类的属性要求补全其余必填字段即可 // 执行待测试方法 $result = $inquiry->sendConfirmationEmail(); // 断言返回结果符合catch分支逻辑 $this->assertFalse($result['success']); $this->assertStringContainsString('Email konnte nicht gesendet werden', $result['message']); $this->assertStringContainsString('SMTP server connection timeout', $result['message']); } }
补充说明
如果你确实需要在KernelTestCase的集成测试场景下覆盖这个分支,可以在启动容器后,将容器内的MailerInterface服务替换为上述配置好的、会抛异常的模拟对象,再执行后续测试逻辑即可。但这种场景纯单元测试是更优选择,不需要启动整个Symfony内核,执行效率会高很多。
内容的提问来源于stack exchange,提问作者thorndeux
相关产品推荐
相关产品推荐

