Symfony 测试实践:隔离内核、替换依赖与验证完整请求

原文:Symfony 官方文档《Testing》,作者归属 Symfony 文档贡献者。未完纪中文翻译整理;原文包括代码示例按 CC BY-SA 3.0 授权,本译编正文与配图也按 CC BY-SA 3.0 提供,已标明编辑修改;不暗示官方背书。

核对日期:2026-10-05。源页 current 对应 Symfony 8.1;官方安装文档要求 PHP 8.4+。本文覆盖源页全部技术主题,配置示例统一展示 YAML;代码仅静态审核,未执行 Composer、PHPUnit、数据库命令或任何测试。

Symfony测试分层示意:单元测试直接检查类,KernelTestCase启动test内核访问测试容器,WebTestCase通过BrowserKit模拟HTTP请求并用Crawler断言,数据库和外部服务使用独立测试资源;JavaScript端到端流程交给真实浏览器。
编辑原创技术示意图,依据本文所列官方源文绘制;非截图。绘制:未完纪编辑整理(Codex 辅助绘制)。

先选对测试层次

Symfony 与 PHPUnit 集成,但不同测试不必都启动整个应用。单元测试检查一个类或方法的局部行为;集成测试检查多个类协作,常需要 Symfony 服务容器;应用测试(也叫功能测试)通过真实或模拟 HTTP 请求检查路由、控制器、服务和视图的整体行为。需要浏览器执行 JavaScript 的完整用户流程时,再使用端到端测试。各项目对术语的边界可能略有不同,这里采用 Symfony 文档定义。

层次 常用基类或工具 主要观察对象
单元 PHPUnit TestCase 一个类或一个方法的输入与输出
集成 KernelTestCase 服务组合、配置与依赖注入
应用 WebTestCase / BrowserKit / Crawler HTTP 响应、页面元素、表单和会话
端到端 Panther 与真实浏览器 包括 JavaScript 的实际浏览流程

安装与目录约定

composer require --dev symfony/test-pack
php bin/phpunit

# 只执行一个目录或一个文件
php bin/phpunit tests/Form
php bin/phpunit tests/Form/UserTypeTest.php

test-pack 安装 PHPUnit 等测试所需包。测试类一般以 Test 结尾,放在 tests/ 目录;单元测试目录通常与 src/ 对应,例如 src/Form/ 下的类对应 tests/Form/。较大的测试集还可以按 Unit、Integration、Application 分类。Composer 自动加载通过 vendor/autoload.php 启用。

当前文档配置文件名为 phpunit.dist.xml;PHPUnit 10 以前常见名称为 phpunit.xml.dist。Symfony Flex 通常生成该文件与 tests/bootstrap.php。若缺失,原文提供 composer recipes:install phpunit/phpunit --force -v 重装配方;编辑提醒:–force 可能覆盖已有配置,应先检查版本控制差异和备份,不把它当作无副作用的修复按钮。具体测试套件与覆盖率配置以当前 PHPUnit 文档为准。

启动独立内核,读取测试容器

集成测试可以继承 KernelTestCase,调用 bootKernel() 启动内核。基类会让每个测试重新启动内核,减少测试之间的状态污染。默认内核类由 .env.test 中的 KERNEL_CLASS 指定;复杂项目也可覆盖 getKernelClass() 或 createKernel(),这两种方式优先于环境变量。

# .env.test
KERNEL_CLASS=App\Kernel
// tests/Service/NewsletterGeneratorTest.php
namespace App\Tests\Service;

use App\Service\NewsletterGenerator;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class NewsletterGeneratorTest extends KernelTestCase
{
    public function testSomething(): void
    {
        self::bootKernel();
        $container = static::getContainer();
        $generator = $container->get(NewsletterGenerator::class);

        // 按实际业务接口提供参数并验证结果。
        $newsletter = $generator->generateMonthlyNews(/* 业务参数 */);
        $this->assertEquals('预期内容', $newsletter->getContent());
    }
}

这是源文业务服务的结构示例,不包含 NewsletterGenerator 的项目实现。static::getContainer() 返回特殊测试容器,可访问 public 服务以及没有被编译器移除的 private 服务。未被任何服务引用而已被移除的私有服务,须在 config/services_test.yaml 中声明为 public,才能在测试中取到;不应为测试方便把生产配置全局改成公开。

test 环境与环境变量的优先级

默认测试内核运行于 test 环境。可在 config/packages/test/ 下放专用配置,或在普通配置中使用 when@test。例如 Twig 开启严格变量模式,尽早暴露模板里不存在的变量:

# config/packages/twig.yaml
when@test:
    twig:
        strict_variables: true

也可以显式覆盖环境与 debug:

self::bootKernel([
    'environment' => 'my_test_env',
    'debug' => false,
]);

源文建议 CI 关闭 debug 以减少测试开销,但这会停用相应自动清理缓存行为。如果每轮没有全新环境,应确保测试缓存确实清理。源文在 tests/bootstrap.php 使用 new \Symfony\Component\Filesystem\Filesystem()->remove(__DIR__.'/../var/cache/test');这是删除操作,只能针对已确认的项目测试缓存路径,不能把动态输入或生产目录拼进来。本次没有执行该代码,也不据此给出速度提升数字。

test 环境会依次读取 .env、.env.test、.env.test.local,后读文件里的同名变量覆盖先前文件。.env.local 在 test 环境不会读取,以减少个人开发配置对测试的一致性影响。系统或 CI 预先注入的环境变量还会影响实际解析结果,因此不能仅看文件名称就判断连接目标。

替换依赖,让服务行为可控

假设 NewsletterGenerator 依赖私有别名 NewsRepositoryInterface,而该别名原本指向 NewsRepository。取出被测服务前,把测试替身放进测试容器即可:

self::bootKernel();
$container = static::getContainer();

$repository = $this->createMock(NewsRepositoryInterface::class);
$repository->expects(self::once())
    ->method('findNewsFromLastMonth')
    ->willReturn([
        new News('some news'),
        new News('some other news'),
    ]);

$container->set(NewsRepositoryInterface::class, $repository);
$generator = $container->get(NewsletterGenerator::class);

片段中的 News、NewsRepositoryInterface 与被测服务由项目提供并正确 use 导入。原文接口位于 App\Contracts\Repository。特殊测试容器支持私有服务与私有别名,不需要因此增加生产公开范围。替换动作应发生在该依赖已被创建或注入之前,避免测试拿到旧实例。

非共享服务每次取用都会创建新实例。Symfony 8.1 增加了用 Closure 工厂替换这类服务的支持,因此要传返回新 mock 的闭包,而不是单个实例:

self::bootKernel();
$container = static::getContainer();

$container->set(Mailer::class, function (): Mailer {
    $mailer = $this->createMock(Mailer::class);
    $mailer->method('send')->willReturn(true);
    return $mailer;
});

$generator = $container->get(NewsletterGenerator::class);

这里 Mailer 指业务服务,方法签名必须允许示例返回值;并不是所有邮件类的 send() 都返回 bool。每次容器解析这个非共享服务都会调用闭包,取得一个新替身。低于 Symfony 8.1 的项目不能直接假设支持这一接口。

数据库必须是独立、可丢弃的测试目标

会访问数据库的测试应使用独立测试库。每位开发者或 CI 工作单元也应避免相互写同一个库。把机器专用连接放在 .env.test.local;若团队共享非秘密的默认配置,可放进受版本管理的 .env.test,真实密码则由本机或 CI 的秘密管理注入。

# .env.test.local:下面是占位语法,不能原样作为真实凭证。
DATABASE_URL="mysql://TEST_USER:TEST_PASSWORD@127.0.0.1:3306/PROJECT_TEST_DB?serverVersion=8.0.37"

源文的 MySQL 8.0.37 参数只是示例,要与实际数据库版本匹配。给库名加 _test 便于识别,但名称后缀不会形成安全隔离。执行以下命令之前,应确认解析后的主机、库名和账号权限都指向一次性测试资源;测试账号不应有生产库权限。

# 仅在已确认的独立测试数据库执行
php bin/console --env=test doctrine:database:create
php bin/console --env=test doctrine:schema:create

这些命令分别创建数据库与映射表结构,可在受控测试 bootstrap 中安排。本次没有连接数据库或执行命令。项目实际还需保证测试结构与生产迁移所形成的结构一致,不能因为 schema:create 成功就跳过迁移测试。

事务回滚与 fixtures 各解决一部分隔离问题

测试应互相独立:一个测试增删实体,不能改变下一个测试的前提。DAMA DoctrineTestBundle 可以在每个测试前开启事务,结束后自动回滚,使同一套测试数据库恢复到测试前状态。安装后按 PHPUnit 版本配置扩展:

composer require --dev dama/doctrine-test-bundle
<!-- phpunit.dist.xml:PHPUnit 10 或更新版本 -->
<phpunit>
    <extensions>
        <bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
    </extensions>
</phpunit>

旧版 PHPUnit 10 以前用 <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>。新旧节点应按所用版本择一,不能因为原文并排展示就一起复制。请同时核对安装的 bundle 与 PHPUnit 兼容性。

隔离边界:事务回滚依赖测试使用的连接及数据库事务行为;外部进程、另一个连接中的提交、非事务表、文件写入、邮件或第三方 HTTP 副作用,不会自动被这次数据库回滚撤销。需要单独的测试资源、替身或清理策略。

fixtures 是可重复加载的人工测试数据,不应从生产库随意拷贝真实用户数据。原文先安装 DoctrineFixturesBundle,再借助 MakerBundle 生成类:

composer require --dev doctrine/doctrine-fixtures-bundle
php bin/console make:fixtures
# 交互时输入 ProductFixture
// src/DataFixtures/ProductFixture.php
namespace App\DataFixtures;

use App\Entity\Product;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;

class ProductFixture extends Fixture
{
    public function load(ObjectManager $manager): void
    {
        $product = new Product();
        $product->setName('Priceless widget');
        $product->setPrice(14.50);
        $product->setDescription('Ok, I guess it *does* have a price');
        $manager->persist($product);
        $manager->flush();
    }
}

保留原文 14.50 是为了说明写入字段,不是在规定金额存储格式。如果 Product 的价格单位是“分”并要求整数,这个值必须依业务约定写为对应整数,例如 1450;若使用 decimal 字符串或 Money 值对象,也须按实体类型改写,避免浮点舍入或单位错配。

以下命令默认会清空目标数据库后重载 fixtures,是破坏性操作。只在确认没有生产连接权限的可丢弃测试库中使用,不以 –env=test 替代目标核验:

php bin/console --env=test doctrine:fixtures:load

第一个应用测试:请求页面再断言

应用测试通常放在 tests/Controller/,继承 WebTestCase。可用 php bin/console make:test,选择 WebTestCase,并给出如 Controller\PostControllerTest 的类名。WebTestCase 在 KernelTestCase 基础上提供浏览器式客户端:

namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class PostControllerTest extends WebTestCase
{
    public function testHomePage(): void
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/');

        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Hello World');
    }
}

createClient() 会启动内核。request() 返回 Crawler,可用 CSS 选择器继续检查,例如 $this->assertCount(4, $crawler->filter('.comment'))。工作流是发请求、点击链接或提交表单、检查响应,再继续下一步。

源文建议在应用测试中直接写面向用户的 URL,例如 /post/hello-world,而不是每次都通过路由器生成;这样路由 URL 被意外修改时,测试能够发现对用户书签与外部链接的影响。若目标是验证路由名本身,也可使用专门断言,两者检验内容不同。

public function request(
    string $method,
    string $uri,
    array $parameters = [],
    array $files = [],
    array $server = [],
    ?string $content = null,
    bool $changeHistory = true
): Crawler

这是客户端方法签名,不是需复制进测试类的实现。它分别接受方法、URL、表单参数、文件、服务器参数、原始正文和是否变更历史。test 环境(或启用了 framework.test 的环境)中,该客户端也以 test.client 服务存在,必要时可替换。

多请求会重新启动内核,禁用重启仍可能重置服务

同一测试的后续请求默认会重启内核,重建容器与服务对象。安全 token 的内存状态、Doctrine 实体的托管状态等可能随之变化。$client->disableReboot() 改为重置内核,但带 kernel.reset 标签的服务仍会调用 reset(),仍可能清掉 token 或使实体脱离管理。

如果某个测试确实需要跨请求保留特定服务状态,原文提供编译器 pass 移除相应标签。以下保留其范围限制,必须只在 test 环境使用;这会减弱隔离,应有明确测试需求,而不是作为所有测试的默认配置:

// src/Kernel.php 中的结构示例
namespace App;

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel implements CompilerPassInterface
{
    use MicroKernelTrait;

    public function process(ContainerBuilder $container): void
    {
        if ('test' === $this->environment) {
            $container->getDefinition('security.token_storage')
                ->clearTag('kernel.reset');
            $container->getDefinition('doctrine')
                ->clearTag('kernel.reset');
        }
    }
}

该片段假定这些服务已由相应组件注册。使用前应与项目已有 Kernel 和 compiler pass 合并,不能覆盖其他项目逻辑。移除重置后还要检查测试之间有没有残留状态。

浏览、重定向与认证

客户端支持 back()、forward()、reload();restart() 清掉 Cookie 与历史。back 和 forward 像普通浏览器一样跳过跳转过程中出现的重定向记录。默认不会自动跟随重定向,可先断言响应再 followRedirect();若要后续请求自动跟随,调用 followRedirects(),传 false 可关闭。

$client->request('GET', '/private');
$this->assertResponseRedirects('/login');
$crawler = $client->followRedirect();

// 根据测试目标决定是否开启:
$client->followRedirects();
$client->followRedirects(false);

测试受保护页面时,每次提交真实登录表单会增加开销。loginUser() 可把测试用户登录状态放进客户端会话。用户应由测试 fixtures 创建,从独立测试库取出;本文添加非空断言,使缺失测试数据时尽早失败:

$client = static::createClient();
$repository = static::getContainer()->get(UserRepository::class);
$testUser = $repository->findOneByEmail('john.doe@example.com');
$this->assertNotNull($testUser);

$client->loginUser($testUser);
$client->request('GET', '/profile');
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Hello John!');

loginUser() 接受 UserInterface 实例,创建 TestBrowserToken 并存进会话。需要 token 自定义属性时使用 tokenAttributes 参数;默认 firewall 为 main,其他 firewall 可传第二个参数,例如 $client->loginUser($testUser, 'my_firewall')。它绕过真正的凭据校验,适合测试授权后的页面,不等于测试登录表单、密码验证或认证器。

原文也演示了 InMemoryUser,并要求在 when@test 下配置匹配的内存用户。示例 admin/password 是公开的弱测试值,不得放进生产用户提供器,也不应在外部可访问环境保留:

use Symfony\Component\Security\Core\User\InMemoryUser;

$testUser = new InMemoryUser('admin', 'password', ['ROLE_ADMIN']);
$client->loginUser($testUser);
# 仅用于隔离测试环境;不是生产安全配置
when@test:
    security:
        providers:
            users_in_memory:
                memory:
                    users:
                        admin: { password: password, roles: ROLE_ADMIN }

loginUser() 不适用于无状态 firewall。这类接口应在每次 request() 中传对应测试 token 或请求头,并保证所用凭据不能访问生产服务。

会话、AJAX 与请求头

可在请求前通过 getSession() 准备会话。原文用固定 CSRF token 说明这一机制:

$client = self::createClient();
$session = $client->getSession();
$session->set('_csrf/form', 'fhr8d5sha3a69tpv24s5');
$session->save();

$client->request('POST', '/form', [
    'form' => ['_token' => 'fhr8d5sha3a69tpv24s5'],
]);

这个公开常量只是受控测试输入,不能成为应用生成 CSRF token 的实现。会话键还需匹配具体应用的 token 存储约定;通常从实际渲染表单中取 token 更能覆盖真实流程。

xmlHttpRequest() 接受与 request() 相同的参数,并自动添加 HTTP_X_REQUESTED_WITH。例如 $client->xmlHttpRequest('POST', '/submit', ['name' => 'Fabien'])。它模拟服务器看到的 AJAX 请求,不会执行页面 JavaScript。

$client = static::createClient([], [
    'HTTP_HOST' => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
]);

$client->request('GET', '/', [], [], [
    'HTTP_HOST' => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
]);

默认头可放在 createClient() 第二个参数,单次覆盖放在 request() 的 server 参数。普通自定义头按 CGI 约定改写:连字符换下划线、大写,并加 HTTP_ 前缀,例如 X-Session-Token 写成 HTTP_X_SESSION_TOKEN。不要把真实 token 写入公开测试代码。

调试异常、查看内部对象与 Profiler

默认客户端捕获应用异常,测试里可能只看到错误响应。调试时调用 $client->catchExceptions(false),让 PHPUnit 直接报告异常。历史与 Cookie 容器分别由 getHistory()、getCookieJar() 返回。

方法 得到的对象
getRequest() / getResponse() 最近一次请求的 HttpKernel Request / Response
getInternalRequest() / getInternalResponse() BrowserKit 自己的请求与响应对象
getCrawler() 当前响应的 Crawler

需要检查数据库查询数等内部指标时,在目标请求之前启用 Profiler:

$client->enableProfiler();
$crawler = $client->request('GET', '/profiler');
$profile = $client->getProfile();

enableProfiler() 针对紧接着的一次请求。具体采集器、允许查询数或性能阈值由项目设置,不是本文替读者测量的结果。测试收集到的数据可能包含敏感请求细节,应按测试数据与日志策略处理。

点击链接与提交表单

clickLink() 点击第一个包含指定文字的链接,也可按可点击图片的 alt 找到链接。若要先检查 Link 对象的方法或 URL,使用 selectLink()->link(),再交给 click():

$crawler = $client->request('GET', '/post/hello-world');
$link = $crawler->selectLink('Click here')->link();
// 可检查 $link->getMethod() 和 $link->getUri()
$client->click($link);

submitForm() 根据表单中的提交按钮定位表单,第一参数可为 button 或 submit input 的文字、id 或 name,第二参数覆盖默认字段值。一张表单可能有多个按钮,因此应选择按钮,不是随便找到 form 标签:

$client->request('GET', '/post/hello-world');
$crawler = $client->submitForm('Add comment', [
    'comment_form[content]' => '测试评论',
]);

更精细的操作可先取得 Form 对象:

$crawler = $client->request('GET', '/post/hello-world');
$form = $crawler->selectButton('submit')->form();
$form['my_form[name]'] = 'Fabien';
$form['my_form[subject]'] = 'Symfony rocks!';
$form['my_form[country]']->select('France');
$form['my_form[like_symfony]']->tick();
$form['my_form[photo]']->upload('/path/to/test-fixture.jpg');
$client->submit($form);

// 或在提交时统一覆盖字段:
$client->submit($form, [
    'my_form[name]' => 'Fabien',
    'my_form[subject]' => 'Symfony rocks!',
]);

select() 用于选项或单选项,tick() 勾选复选框,upload() 填充上传文件。多文件字段可分别访问 my_form[field][0]、my_form[field][1]。示例路径只是字段 API 的结构示意,实际应指向随测试提供、可安全上传的专用文件,不能读取个人或生产文件。上述两次 submit 是两种替代写法,正常测试应按需求择一。

Form 的 getName() 可以取得表单名,减少字段名前缀硬编码;getUri()、getValues()、getFiles() 分别返回目标地址、字段值和文件。getPhpValues() 与 getPhpFiles() 将带方括号的字段名转换为 PHP 数组结构。若故意测试无效 select/radio 值,需按 DomCrawler 文档使用相关接口,而不能假设正常选择方法允许任意值。

$client->submit($form, [], ['HTTP_ACCEPT_LANGUAGE' => 'es']);
$client->submitForm($button, [], 'POST', ['HTTP_ACCEPT_LANGUAGE' => 'es']);

这些可选参数允许提交时提供服务器参数和请求头。BrowserKit 与 Crawler 只模拟 HTTP 与 HTML 交互;需要检查 JavaScript 动态更新、客户端路由或真实浏览器行为时,使用 Panther 端到端测试。

Symfony 断言目录:按你想验证的对象选择

所有基于 PHPUnit 的测试都可用普通 PHPUnit 断言。Symfony 在此基础上增加以下断言。除另行注明外,断言通常接受可选失败消息;同一行的 Not 版本表达反向条件。下表按源页完整列出方法族,省略重复 PHP 类型签名以便查阅。

响应、请求、浏览器断言由 WebTestCase、BrowserKitAssertionsTrait 或 WebTestAssertionsTrait 提供。响应断言检查最近一次响应,浏览器 Cookie 断言检查测试客户端累计保存的 Cookie,两者不要混淆。

响应断言 含义
assertResponseIsSuccessful HTTP 状态在 2xx 范围
assertResponseStatusCodeSame 状态码等于指定整数
assertResponseRedirects 验证重定向;可附带目标地址(绝对或相对)和状态码
assertResponseHasHeader / assertResponseNotHasHeader 指定响应头是否存在
assertResponseHeaderSame / assertResponseHeaderNotSame 响应头值是否等于预期值
assertResponseHasCookie / assertResponseNotHasCookie 响应是否设置指定 Cookie,可指定 path(默认 /)与 domain
assertResponseCookieValueSame 指定 Cookie 值与预期相同,可限定 path/domain
assertResponseFormatSame 通过 Request::getFormat() 根据响应 Content-Type 得到的格式是否与预期相同
assertResponseIsUnprocessable HTTP 状态为 422

成功状态、指定状态码、重定向和 422 断言可控制 verbose 失败细节;源页 422 签名显示的 bool ?$verbose 属于类型顺序笔误,应为 ?bool $verbose。全局可用 BrowserKitAssertionsTrait::setBrowserKitAssertionsAsVerbose(false) 关闭详细输出。

请求或浏览器断言 含义
assertRequestAttributeValueSame 请求 attribute 的值符合预期
assertRouteSame 匹配指定路由,可附带路由参数
assertSessionHasFlashMessage flash bag 中指定类型至少包含给定消息之一;Symfony 8.1 新增
assertBrowserHasCookie / assertBrowserNotHasCookie 测试客户端是否持有 Cookie,包括此前响应设置的 Cookie
assertBrowserCookieValueSame 客户端指定 Cookie 的值符合预期
assertBrowserHistoryIsOnFirstPage / assertBrowserHistoryIsNotOnFirstPage 历史指针是否位于第一页
assertBrowserHistoryIsOnLastPage / assertBrowserHistoryIsNotOnLastPage 历史指针是否位于最后一页
assertThatForClient 对客户端应用自定义 PHPUnit Constraint,便于封装自定义断言

DOM 断言可由 WebTestCase、DomCrawlerAssertionsTrait 或 WebTestAssertionsTrait 提供。注意“第一个匹配元素”和“任意匹配元素”的区别:

Crawler 断言 含义
assertSelectorExists / assertSelectorNotExists CSS 选择器是否至少匹配一个元素
assertSelectorCount 匹配元素的数量
assertSelectorTextContains / assertSelectorTextNotContains 第一个匹配元素是否包含指定文本
assertAnySelectorTextContains / assertAnySelectorTextNotContains 任意匹配元素是否满足相应文本约束
assertSelectorTextSame / assertAnySelectorTextSame 第一个或任意匹配元素的文本与预期完全相同
assertPageTitleSame / assertPageTitleContains title 完全相同或包含文本
assertInputValueSame / assertInputValueNotSame 指定 name 的输入字段值是否相同
assertCheckboxChecked / assertCheckboxNotChecked 复选框是否勾选
assertFormValue / assertNoFormValue 第一个匹配表单中,字段值是否等于指定值

邮件断言由 KernelTestCase 或 MailerAssertionsTrait 提供;这些断言本身不保证邮件没有发往真实收件人。测试环境仍应配置隔离 transport 或替身。

Mailer 断言 含义
assertEmailCount / assertQueuedEmailCount 已发送或已入队邮件数,可限定 transport
assertEmailIsQueued / assertEmailIsNotQueued Mailer MessageEvent 是否入队;可用 getMailerEvent(index=0, transport=null) 取事件
assertEmailAttachmentCount 附件数量;可用 getMailerMessage(index=0, transport=null) 取邮件
assertEmailTextBodyContains / assertEmailTextBodyNotContains 纯文本正文是否包含指定文本
assertEmailHtmlBodyContains / assertEmailHtmlBodyNotContains HTML 正文是否包含指定文本
assertEmailHasHeader / assertEmailNotHasHeader 邮件头是否存在
assertEmailHeaderSame / assertEmailHeaderNotSame 邮件头值是否符合预期
assertEmailAddressContains / assertEmailAddressNotContains 地址头是否包含给定地址,先把姓名加尖括号地址规范化为实际邮箱
assertEmailSubjectContains / assertEmailSubjectNotContains 主题是否包含指定内容

通知断言由 KernelTestCase 或 NotificationAssertionsTrait 提供:

Notifier 断言 含义
assertNotificationCount / assertQueuedNotificationCount 全部或指定 transport 的通知数量、入队数量
assertNotificationIsQueued / assertNotificationIsNotQueued 通知 MessageEvent 是否已入队
assertNotificationSubjectContains / assertNotificationSubjectNotContains 通知主题是否包含文本
assertNotificationTransportIsEqual / assertNotificationTransportIsNotEqual 通知的 transport 名称是否相同

HttpClient 断言由 WebTestCase、HttpClientAssertionsTrait 或 WebTestAssertionsTrait 提供。在触发出站 HTTP 请求的代码之前,必须调用 $client->enableProfiler();记录和断言请求不等于阻止真实网络访问,测试仍需 MockHttpClient 或隔离服务等对应方案。

HttpClient 断言 含义与默认值
assertHttpClientRequest 验证某 URL 已被调用;可检查 method(默认 GET)、body、headers 和 httpClientId(默认 http_client);多次调用中只要存在匹配调用即可通过
assertNotHttpClientRequest 验证没有以指定方法调用 URL;method 默认 GET,可选客户端 ID
assertHttpClientRequestCount 验证指定客户端发出的请求总数

Symfony 8.1 还增加了 Console 断言,输入为 ExecutionResult。它们用于检查命令执行结果;如何在测试中调用命令,参见 Console 测试文档。

Console 断言 含义
assertCommandIsSuccessful 命令成功结束
assertCommandFailed 命令执行失败
assertCommandIsInvalid 命令以无效输入等对应代码结束
assertCommandResultEquals 按需要同时核对状态码、标准输出、错误输出与显示结果,未指定的预期项可为空

编辑核对(2026-10-05):源页对 assertResponseFormatSame 的方法归属存在笔误。Symfony 8.1 官方约束实现调用的是 Request::getFormat(),参数来自响应的 Content-Type;上表据此更正。

把可重复性留在测试设计里

这一套流程既需要清晰的断言,也需要可靠的边界:每轮内核和服务状态可控、数据库只包含可丢弃测试数据、请求和副作用不触及真实用户、仅在需要时放宽重置行为。loginUser 不能代替认证流程测试,BrowserKit 不能代替 JavaScript 浏览器测试,事务回滚不能代替所有系统副作用清理。

源页还提供 自定义测试 bootstrap、Doctrine 仓库测试、DOM Crawler、多个客户端交互 与 功能测试中的 Profiler 延伸阅读。这些链接是进一步的专项文档,不表示本次额外执行过相关测试。

本文已静态审查展示代码中的数据库清空、配置覆盖、缓存删除、公开弱测试值与隔离失效风险;没有执行教程命令,也没有声称这些片段可直接适配任意项目。未发现其他问题不等于不存在漏洞。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容