原文: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 与 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 延伸阅读。这些链接是进一步的专项文档,不表示本次额外执行过相关测试。
本文已静态审查展示代码中的数据库清空、配置覆盖、缓存删除、公开弱测试值与隔离失效风险;没有执行教程命令,也没有声称这些片段可直接适配任意项目。未发现其他问题不等于不存在漏洞。












暂无评论内容