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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 21:57:22