跳到内容

测试

编辑此页

每当你编写新代码时,你也可能引入新的错误。为了构建更好、更可靠的应用,你应该使用功能测试和单元测试来测试你的代码。

PHPUnit 测试框架

Symfony 集成了一个名为 PHPUnit 的独立库,为你提供丰富的测试框架。本文不会涵盖 PHPUnit 本身,它有自己优秀的 文档

在创建你的第一个测试之前,安装 symfony/test-pack,它会安装一些其他测试所需的包(例如 phpunit/phpunit

1
$ composer require --dev symfony/test-pack

库安装完成后,尝试运行 PHPUnit

1
$ php bin/phpunit

此命令会自动运行你的应用测试。每个测试都是一个以 “Test” 结尾的 PHP 类(例如 BlogControllerTest),位于你应用的 tests/ 目录中。

PHPUnit 通过你应用根目录下的 phpunit.xml.dist 文件进行配置。在大多数情况下,Symfony Flex 提供的默认配置就足够了。阅读 PHPUnit 文档以了解所有可能的配置选项(例如,启用代码覆盖率或将你的测试拆分为多个“测试套件”)。

注意

Symfony Flex 会自动创建 phpunit.xml.disttests/bootstrap.php。如果这些文件丢失,你可以尝试使用 composer recipes:install phpunit/phpunit --force -v 再次运行 recipe。

测试类型

自动化测试有很多类型,精确的定义通常因项目而异。在 Symfony 中,使用以下定义。如果你学到了一些不同的东西,那不一定错,只是与 Symfony 文档使用的定义不同。

单元测试
这些测试确保源代码的 *单个* 单元(例如,单个类)按预期运行。
集成测试
这些测试测试类的组合,并且通常与 Symfony 的服务容器交互。这些测试尚未涵盖完全正常运行的应用,那些被称为 *应用测试*。
应用测试
应用测试测试完整应用的 behavior。它们发送 HTTP 请求(真实的和模拟的),并测试响应是否符合预期。

单元测试

单元测试 确保源代码的单个单元(例如,某个类或某个类中的特定方法)符合其设计并按预期运行。在 Symfony 应用中编写单元测试与编写标准的 PHPUnit 单元测试没有区别。你可以在 PHPUnit 文档中了解更多信息: Writing Tests for PHPUnit

按照惯例,tests/ 目录应该复制你应用的目录结构用于单元测试。因此,如果你要测试 src/Form/ 目录中的一个类,请将测试放在 tests/Form/ 目录中。自动加载通过 vendor/autoload.php 文件自动启用(默认在 phpunit.xml.dist 文件中配置)。

你可以使用 bin/phpunit 命令运行测试

1
2
3
4
5
6
7
8
# run all tests of the application
$ php bin/phpunit

# run all tests in the Form/ directory
$ php bin/phpunit tests/Form

# run tests for the UserType class
$ php bin/phpunit tests/Form/UserTypeTest.php

提示

在大型测试套件中,为每种类型的测试创建子目录可能更有意义(tests/Unit/tests/Integration/tests/Application/ 等)。

集成测试

与单元测试相比,集成测试将测试你应用的更大部分(例如,服务的组合)。集成测试可能需要使用 Symfony Kernel 从依赖注入容器中获取服务。

Symfony 提供了一个 KernelTestCase 类,以帮助你在测试中使用 bootKernel() 创建和启动内核

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// tests/Service/NewsletterGeneratorTest.php
namespace App\Tests\Service;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class NewsletterGeneratorTest extends KernelTestCase
{
    public function testSomething(): void
    {
        self::bootKernel();

        // ...
    }
}

KernelTestCase 还确保你的内核在每次测试时都被重启。这保证了每个测试彼此独立运行。

要运行你的应用测试,KernelTestCase 类需要找到要初始化的应用内核。内核类通常在 KERNEL_CLASS 环境变量中定义(包含在 Symfony Flex 提供的默认 .env.test 文件中)

1
2
# .env.test
KERNEL_CLASS=App\Kernel

注意

如果你的用例更复杂,你还可以覆盖你的功能测试的 getKernelClass()createKernel() 方法,这将优先于 KERNEL_CLASS 环境变量。

设置测试环境

测试创建一个在 test 环境中运行的内核。这允许你在 config/packages/test/ 中为你的测试设置特殊配置。

如果你安装了 Symfony Flex,一些包已经安装了一些有用的测试配置。例如,默认情况下,Twig bundle 被配置为特别严格,以便在将代码部署到生产环境之前捕获错误

1
2
3
# config/packages/test/twig.yaml
twig:
    strict_variables: true

你也可以完全使用不同的环境,或者通过将每个环境作为选项传递给 bootKernel() 方法来覆盖默认的 debug 模式 (true)

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

提示

建议在你的 CI 服务器上以 debug 设置为 false 运行测试,因为这可以显著提高测试性能。这将禁用清除缓存。如果你的测试不是每次都在干净的环境中运行,你必须手动清除缓存,例如在 tests/bootstrap.php 中使用以下代码

1
2
3
4
// ...

// ensure a fresh cache when debug mode is disabled
(new \Symfony\Component\Filesystem\Filesystem())->remove(__DIR__.'/../var/cache/test');

自定义环境变量

如果你需要为你的测试自定义一些环境变量(例如 Doctrine 使用的 DATABASE_URL),你可以通过在你的 .env.test 文件中覆盖任何你需要的内容来做到这一点

1
2
3
4
# .env.test

# ...
DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name_test?serverVersion=8.0.37"

在测试环境中,会读取以下 env 文件(如果变量在其中重复,列表中较低的文件会覆盖之前的项)

  1. .env:包含应用默认值的环境变量;
  2. .env.test:覆盖/设置特定的测试值或变量;
  3. .env.test.local:覆盖特定于此机器的设置。

警告

.env.local 文件在测试环境中 *不* 使用,以使每个测试设置尽可能一致。

在测试中获取服务

在你的集成测试中,你通常需要从服务容器中获取服务以调用特定方法。启动内核后,容器由 static::getContainer() 返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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
    {
        // (1) boot the Symfony kernel
        self::bootKernel();

        // (2) use static::getContainer() to access the service container
        $container = static::getContainer();

        // (3) run some service & test the result
        $newsletterGenerator = $container->get(NewsletterGenerator::class);
        $newsletter = $newsletterGenerator->generateMonthlyNews(/* ... */);

        $this->assertEquals('...', $newsletter->getContent());
    }
}

来自 static::getContainer() 的容器实际上是一个特殊的测试容器。它允许你访问公共服务和未删除的 私有服务

注意

如果你需要测试已被删除的私有服务(那些不被任何其他服务使用的服务),你需要在 config/services_test.yaml 文件中将这些私有服务声明为公共服务。

模拟依赖

有时,模拟被测服务的依赖项可能很有用。从上一节的示例中,假设 NewsletterGenerator 依赖于指向私有 NewsRepository 服务的私有别名 NewsRepositoryInterface,并且你想使用模拟的 NewsRepositoryInterface 而不是具体的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ...
use App\Contracts\Repository\NewsRepositoryInterface;

class NewsletterGeneratorTest extends KernelTestCase
{
    public function testSomething(): void
    {
        // ... same bootstrap as the section above

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

        $container->set(NewsRepositoryInterface::class, $newsRepository);

        // will be injected the mocked repository
        $newsletterGenerator = $container->get(NewsletterGenerator::class);

        // ...
    }
}

不需要进一步的配置,因为测试服务容器是一个特殊的容器,它允许你与私有服务和别名进行交互。

为测试配置数据库

与数据库交互的测试应该使用它们自己独立的数据库,以避免干扰其他 配置环境中使用的数据。

为此,编辑或创建你项目根目录下的 .env.test.local 文件,并为 DATABASE_URL 环境变量定义新值

1
2
# .env.test.local
DATABASE_URL="mysql://USERNAME:PASSWORD@127.0.0.1:3306/DB_NAME?serverVersion=8.0.37"

这假设每个开发者/机器都为测试使用不同的数据库。如果每个机器上的测试设置都相同,请改用 .env.test 文件并将其提交到共享仓库。了解更多关于 在 Symfony 应用中使用多个 .env 文件的信息。

之后,你可以使用以下命令创建测试数据库和所有表

1
2
3
4
5
# create the test database
$ php bin/console --env=test doctrine:database:create

# create the tables/columns in the test database
$ php bin/console --env=test doctrine:schema:create

提示

你可以在 测试引导过程 中运行这些命令来创建数据库。

提示

常见的做法是在测试中将 _test 后缀附加到原始数据库名称。如果生产环境中的数据库名称称为 project_acme,则测试数据库的名称可以是 project_acme_test

在每次测试之前自动重置数据库

测试应该彼此独立,以避免副作用。例如,如果某个测试修改了数据库(通过添加或删除实体),则可能会更改其他测试的结果。

DAMADoctrineTestBundle 使用 Doctrine 事务,让每个测试都与未修改的数据库进行交互。使用以下命令安装它

1
$ composer require --dev dama/doctrine-test-bundle

现在,将其作为 PHPUnit 扩展启用

1
2
3
4
5
6
7
8
<!-- phpunit.xml.dist -->
<phpunit>
    <!-- ... -->

    <extensions>
        <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
    </extensions>
</phpunit>

就是这样!这个 bundle 使用了一个巧妙的技巧:它在每次测试之前启动一个数据库事务,并在测试完成后自动回滚它以撤消所有更改。在 DAMADoctrineTestBundle 的文档中阅读更多内容。

加载虚拟数据 Fixture

与使用生产数据库中的真实数据不同,通常在测试数据库中使用虚假或虚拟数据。这通常被称为“fixture 数据”,Doctrine 提供了一个库来创建和加载它们。使用以下命令安装它

1
$ composer require --dev doctrine/doctrine-fixtures-bundle

然后,使用 SymfonyMakerBundlemake:fixtures 命令生成一个空的 fixture 类

1
2
3
4
$ php bin/console make:fixtures

The class name of the fixtures to create (e.g. AppFixtures):
> ProductFixture

然后你修改并使用这个类将新实体加载到数据库中。例如,要将 Product 对象加载到 Doctrine 中,请使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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);

        // add more products

        $manager->flush();
    }
}

清空数据库并重新加载 *所有* fixture 类,使用

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

有关更多信息,请阅读 DoctrineFixturesBundle 文档

应用测试

应用测试检查应用所有不同层(从路由到视图)的集成。就 PHPUnit 而言,它们与单元测试或集成测试没有什么不同,但它们有一个非常特定的工作流程

  1. 发送请求;
  2. 与页面交互(例如,点击链接或提交表单);
  3. 测试响应;
  4. 重复上述步骤。

注意

本节中使用的工具可以通过 symfony/test-pack 安装,如果你还没有安装,请使用 composer require symfony/test-pack

编写你的第一个应用测试

应用测试是 PHP 文件,通常位于你应用的 tests/Controller/ 目录中。它们通常继承 WebTestCase。这个类在 KernelTestCase 之上添加了特殊的逻辑。你可以在上面关于 集成测试 的部分阅读更多相关信息。

如果你想测试由你的 PostController 类处理的页面,首先使用 SymfonyMakerBundlemake:test 命令创建一个新的 PostControllerTest

1
2
3
4
5
6
7
$ php bin/console make:test

 Which test type would you like?:
 > WebTestCase

 The name of the test class (e.g. BlogPostTest):
 > Controller\PostControllerTest

这将创建以下测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// tests/Controller/PostControllerTest.php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class PostControllerTest extends WebTestCase
{
    public function testSomething(): void
    {
        // This calls KernelTestCase::bootKernel(), and creates a
        // "client" that is acting as the browser
        $client = static::createClient();

        // Request a specific page
        $crawler = $client->request('GET', '/');

        // Validate a successful response and some content
        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Hello World');
    }
}

在上面的示例中,测试验证 HTTP 响应是否成功,并且请求体包含一个带有 “Hello world” 的 <h1> 标签。

request() 方法还返回一个爬虫 (crawler),你可以使用它在你的测试中创建更复杂的断言(例如,计算与给定 CSS 选择器匹配的页面元素数量)

1
2
$crawler = $client->request('GET', '/post/hello-world');
$this->assertCount(4, $crawler->filter('.comment'));

你可以在 DOM 爬虫 中了解更多关于爬虫的信息。

发送请求

测试客户端模拟一个像浏览器一样的 HTTP 客户端,并向您的 Symfony 应用程序发出请求

1
$crawler = $client->request('GET', '/post/hello-world');

request() 方法接受 HTTP 方法和 URL 作为参数,并返回一个 Crawler 实例。

提示

硬编码请求 URL 是应用程序测试的最佳实践。如果测试使用 Symfony 路由器生成 URL,则它不会检测到对应用程序 URL 所做的任何可能影响最终用户的更改。

request() 方法的完整签名是

1
2
3
4
5
6
7
8
9
public function request(
    string $method,
    string $uri,
    array $parameters = [],
    array $files = [],
    array $server = [],
    ?string $content = null,
    bool $changeHistory = true
): Crawler

这允许您创建您可以想到的所有类型的请求

提示

测试客户端在 test 环境(或启用 framework.test 选项的任何地方)中作为容器中的 test.client 服务提供。这意味着如果需要,您可以完全覆盖该服务。

在一个测试中进行多次请求

发出请求后,后续请求将使客户端重新启动内核。这将从头开始重新创建容器,以确保请求是隔离的,并且每次都使用新的服务对象。此行为可能会产生一些意想不到的后果:例如,安全令牌将被清除,Doctrine 实体将被分离等等。

首先,您可以调用客户端的 disableReboot() 方法来重置内核而不是重新启动它。实际上,Symfony 将调用标记为 kernel.reset 的每个服务的 reset() 方法。但是,这会清除安全令牌,分离 Doctrine 实体等。

为了解决这个问题,创建一个 编译器传递 以从您的测试环境中的某些服务中删除 kernel.reset 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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) {
            // prevents the security token to be cleared
            $container->getDefinition('security.token_storage')->clearTag('kernel.reset');

            // prevents Doctrine entities to be detached
            $container->getDefinition('doctrine')->clearTag('kernel.reset');

            // ...
        }
    }
}

浏览站点

客户端支持在真实浏览器中可以完成的许多操作

1
2
3
4
5
6
$client->back();
$client->forward();
$client->reload();

// clears all cookies and the history
$client->restart();

注意

back()forward() 方法会跳过在请求 URL 时可能发生的重定向,就像普通浏览器一样。

重定向

当请求返回重定向响应时,客户端不会自动跟随它。您可以检查响应,然后使用 followRedirect() 方法强制重定向

1
$crawler = $client->followRedirect();

如果您希望客户端自动跟随所有重定向,您可以在执行请求之前调用 followRedirects() 方法来强制执行它们

1
$client->followRedirects();

如果您将 false 传递给 followRedirects() 方法,则将不再跟随重定向

1
$client->followRedirects(false);

用户登录(身份验证)

当您想为受保护的页面添加应用程序测试时,您必须首先以用户身份“登录”。重现实际步骤(例如提交登录表单)会使测试非常缓慢。因此,Symfony 提供了一个 loginUser() 方法来模拟功能测试中的登录。

建议不要使用真实用户登录,而是仅为测试创建一个用户。您可以使用 Doctrine 数据 fixtures 仅在测试数据库中加载测试用户。

在数据库中加载用户后,使用您的用户仓库获取此用户,并使用 $client->loginUser() 来模拟登录请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// tests/Controller/ProfileControllerTest.php
namespace App\Tests\Controller;

use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class ProfileControllerTest extends WebTestCase
{
    // ...

    public function testVisitingWhileLoggedIn(): void
    {
        $client = static::createClient();
        $userRepository = static::getContainer()->get(UserRepository::class);

        // retrieve the test user
        $testUser = $userRepository->findOneByEmail('john.doe@example.com');

        // simulate $testUser being logged in
        $client->loginUser($testUser);

        // test e.g. the profile page
        $client->request('GET', '/profile');
        $this->assertResponseIsSuccessful();
        $this->assertSelectorTextContains('h1', 'Hello John!');
    }
}

您可以将任何 UserInterface 实例传递给 loginUser()loginUser() 方法创建一个特殊的 TestBrowserToken 对象并将其存储在测试客户端的会话中。如果您需要在该令牌中定义自定义属性,则可以使用 loginUser() 方法的 tokenAttributes 参数。

要设置特定的防火墙(默认情况下设置为 main

1
$client->loginUser($testUser, 'my_firewall');

注意

根据设计,当使用无状态防火墙时,loginUser() 方法不起作用。相反,在每个 request() 调用中添加相应的令牌/标头。

发出 AJAX 请求

客户端提供了一个 xmlHttpRequest() 方法,该方法与 request() 方法具有相同的参数,并且是发出 AJAX 请求的快捷方式

1
2
// the required HTTP_X_REQUESTED_WITH header is added automatically
$client->xmlHttpRequest('POST', '/submit', ['name' => 'Fabien']);

发送自定义标头

如果您的应用程序根据某些 HTTP 标头的行为,请将它们作为 createClient() 的第二个参数传递

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

您还可以按请求覆盖 HTTP 标头

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

警告

您的自定义标头的名称必须遵循 RFC 3875 的第 4.1.18 节中定义的语法:将 - 替换为 _,将其转换为大写,并在结果前加上 HTTP_。例如,如果您的标头名称是 X-Session-Token,则传递 HTTP_X_SESSION_TOKEN

报告异常

调试应用程序测试中的异常可能很困难,因为默认情况下它们会被捕获,您需要查看日志以查看抛出了哪个异常。禁用测试客户端中异常的捕获允许 PHPUnit 报告异常

1
$client->catchExceptions(false);

访问内部对象

如果您使用客户端来测试您的应用程序,您可能希望访问客户端的内部对象

1
2
$history = $client->getHistory();
$cookieJar = $client->getCookieJar();

您还可以获取与最新请求相关的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// the HttpKernel request instance
$request = $client->getRequest();

// the BrowserKit request instance
$request = $client->getInternalRequest();

// the HttpKernel response instance
$response = $client->getResponse();

// the BrowserKit response instance
$response = $client->getInternalResponse();

// the Crawler instance
$crawler = $client->getCrawler();

访问分析器数据

在每个请求中,您可以启用 Symfony 分析器来收集有关该请求的内部处理的数据。例如,分析器可用于验证给定页面在加载时运行的数据库查询是否少于某个数量。

要获取上次请求的分析器,请执行以下操作

1
2
3
4
5
6
7
// enables the profiler for the very next request
$client->enableProfiler();

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

// gets the profile
$profile = $client->getProfile();

有关在测试中使用分析器的具体详细信息,请参阅 如何在功能测试中使用分析器 文章。

与响应交互

像真正的浏览器一样,客户端和 Crawler 对象可用于与您收到的页面进行交互

使用 clickLink() 方法单击包含给定文本的第一个链接(或带有该 alt 属性的第一个可点击图像)

1
2
3
4
$client = static::createClient();
$client->request('GET', '/post/hello-world');

$client->clickLink('Click here');

如果您需要访问 Link 对象,该对象提供特定于链接的有用方法(例如 getMethod()getUri()),请改用 Crawler::selectLink() 方法

1
2
3
4
5
6
7
8
$client = static::createClient();
$crawler = $client->request('GET', '/post/hello-world');

$link = $crawler->selectLink('Click here')->link();
// ...

// use click() if you want to click the selected link
$client->click($link);

提交表单

使用 submitForm() 方法提交包含给定按钮的表单

1
2
3
4
5
6
$client = static::createClient();
$client->request('GET', '/post/hello-world');

$crawler = $client->submitForm('Add comment', [
    'comment_form[content]' => '...',
]);

submitForm() 的第一个参数是任何包含在表单中的 <button><input type="submit"> 的文本内容、idvaluename。第二个可选参数用于覆盖默认表单字段值。

注意

请注意,您选择的是表单按钮而不是表单,因为一个表单可以有多个按钮。如果您使用遍历 API,请记住您必须查找按钮。

如果您需要访问 Form 对象,该对象提供特定于表单的有用方法(例如 getUri()getValues()getFiles()),请改用 Crawler::selectButton() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$client = static::createClient();
$crawler = $client->request('GET', '/post/hello-world');

// select the button
$buttonCrawlerNode = $crawler->selectButton('submit');

// retrieve the Form object for the form belonging to this button
$form = $buttonCrawlerNode->form();

// set values on a form object
$form['my_form[name]'] = 'Fabien';
$form['my_form[subject]'] = 'Symfony rocks!';

// submit the Form object
$client->submit($form);

// optionally, you can combine the last 2 steps by passing an array of
// field values while submitting the form:
$client->submit($form, [
    'my_form[name]'    => 'Fabien',
    'my_form[subject]' => 'Symfony rocks!',
]);

根据表单类型,您可以使用不同的方法来填写输入

1
2
3
4
5
6
7
8
9
10
11
12
// selects an option or a radio
$form['my_form[country]']->select('France');

// ticks a checkbox
$form['my_form[like_symfony]']->tick();

// uploads a file
$form['my_form[photo]']->upload('/path/to/lucas.jpg');

// In the case of a multiple file upload
$form['my_form[field][0]']->upload('/path/to/lucas.jpg');
$form['my_form[field][1]']->upload('/path/to/lisa.jpg');

提示

您可以不将表单名称硬编码为字段名称的一部分(例如,在前面的示例中为 my_form[...]),而是可以使用 getName() 方法来获取表单名称。

提示

如果您有意选择“无效”的 select/radio 值,请参阅 DomCrawler 组件

提示

您可以通过调用 Form 对象上的 getValues() 方法来获取将要提交的值。上传的文件在 getFiles() 返回的单独数组中可用。getPhpValues()getPhpFiles() 方法也返回提交的值,但采用 PHP 格式(它将带有方括号表示法的键 - 例如 my_form[subject] - 转换为 PHP 数组)。

提示

submit()submitForm() 方法定义了可选参数,用于在提交表单时添加自定义服务器参数和 HTTP 标头

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

测试响应 (断言)

现在测试已经访问了一个页面并与之交互(例如填写了表单),现在是验证是否显示了预期输出的时候了。

由于所有测试都基于 PHPUnit,因此您可以在测试中使用任何 PHPUnit 断言。结合测试客户端和 Crawler,这允许您检查任何您想要的内容。

但是,Symfony 为最常见的情况提供了有用的快捷方法

响应断言

assertResponseIsSuccessful(string $message = '', bool $verbose = true)
断言响应成功(HTTP 状态为 2xx)。
assertResponseStatusCodeSame(int $expectedCode, string $message = '', bool $verbose = true)
断言特定的 HTTP 状态代码。
assertResponseRedirects(?string $expectedLocation = null, ?int $expectedCode = null, string $message = '', bool $verbose = true)
断言响应是重定向响应(可选地,您可以检查目标位置和状态代码)。排除的位置可以是绝对路径或相对路径。
assertResponseHasHeader(string $headerName, string $message = '')/assertResponseNotHasHeader(string $headerName, string $message = '')
断言给定的标头在响应中(不)可用,例如 assertResponseHasHeader('content-type');
assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = '')/assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = '')
断言给定的标头在响应中(不)包含预期值,例如 assertResponseHeaderSame('content-type', 'application/octet-stream');
assertResponseHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')/assertResponseNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')
断言给定的 cookie 存在于响应中(可选地检查特定的 cookie 路径或域)。
assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = '')
断言给定的 cookie 存在并设置为预期值。
assertResponseFormatSame(?string $expectedFormat, string $message = '')
断言 getFormat() 方法返回的响应格式与预期值相同。
assertResponseIsUnprocessable(string $message = '', bool $verbose = true)
断言响应是不可处理的(HTTP 状态为 422)

7.1

$verbose 参数在 Symfony 7.1 中引入。

请求断言

assertRequestAttributeValueSame(string $name, string $expectedValue, string $message = '')
断言给定的 请求属性 设置为预期值。
assertRouteSame($expectedRoute, array $parameters = [], string $message = '')
断言请求与给定的路由和可选的路由参数匹配。

浏览器断言

assertBrowserHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')/assertBrowserNotHasCookie(string $name, string $path = '/', ?string $domain = null, string $message = '')
断言测试客户端(不)具有给定的 cookie 设置(意味着 cookie 是由测试中的任何响应设置的)。
assertBrowserCookieValueSame(string $name, string $expectedValue, string $path = '/', ?string $domain = null, string $message = '')
断言测试客户端中给定的 cookie 设置为预期值。
assertThatForClient(Constraint $constraint, string $message = '')

在客户端中断言给定的约束。用于以与内置断言相同的方式使用您的自定义断言(即,无需传递客户端作为参数)

1
2
3
4
5
// add this method in some custom class imported in your tests
protected static function assertMyOwnCustomAssert(): void
{
    self::assertThatForClient(new SomeCustomConstraint());
}

Crawler 断言

assertSelectorExists(string $selector, string $message = '')/assertSelectorNotExists(string $selector, string $message = '')
断言给定的选择器在响应中(不)匹配至少一个元素。
assertSelectorCount(int $expectedCount, string $selector, string $message = '')
断言响应中存在预期数量的选择器元素
assertSelectorTextContains(string $selector, string $text, string $message = '')/assertSelectorTextNotContains(string $selector, string $text, string $message = '')
断言与给定选择器匹配的第一个元素(不)包含预期的文本。
assertAnySelectorTextContains(string $selector, string $text, string $message = '')/assertAnySelectorTextNotContains(string $selector, string $text, string $message = '')
断言与给定选择器匹配的任何元素(不)包含预期的文本。
assertSelectorTextSame(string $selector, string $text, string $message = '')
断言与给定选择器匹配的第一个元素的内容等于预期的文本。
assertAnySelectorTextSame(string $selector, string $text, string $message = '')
断言与给定选择器匹配的任何元素等于预期的文本。
assertPageTitleSame(string $expectedTitle, string $message = '')
断言 <title> 元素等于给定的标题。
assertPageTitleContains(string $expectedTitle, string $message = '')
断言 <title> 元素包含给定的标题。
assertInputValueSame(string $fieldName, string $expectedValue, string $message = '')/assertInputValueNotSame(string $fieldName, string $expectedValue, string $message = '')
断言具有给定名称的表单输入的值(不)等于预期值。
assertCheckboxChecked(string $fieldName, string $message = '')/assertCheckboxNotChecked(string $fieldName, string $message = '')
断言具有给定名称的复选框是(未)选中状态。
assertFormValue(string $formSelector, string $fieldName, string $value, string $message = '')/assertNoFormValue(string $formSelector, string $fieldName, string $message = '')
断言与给定选择器匹配的第一个表单的字段值(不)等于预期值。

Mailer 断言

assertEmailCount(int $count, ?string $transport = null, string $message = '')
断言已发送的电子邮件的预期数量。
assertQueuedEmailCount(int $count, ?string $transport = null, string $message = '')
断言已排队的电子邮件的预期数量(例如,使用 Messenger 组件)。
assertEmailIsQueued(MessageEvent $event, string $message = '')/assertEmailIsNotQueued(MessageEvent $event, string $message = '')
断言给定的邮件程序事件是(未)排队。使用 getMailerEvent(int $index = 0, ?string $transport = null) 按索引检索邮件程序事件。
assertEmailAttachmentCount(RawMessage $email, int $count, string $message = '')
断言给定的电子邮件具有预期数量的附件。使用 getMailerMessage(int $index = 0, ?string $transport = null) 按索引检索特定的电子邮件。
assertEmailTextBodyContains(RawMessage $email, string $text, string $message = '')/assertEmailTextBodyNotContains(RawMessage $email, string $text, string $message = '')
断言给定电子邮件的文本正文(不)包含预期的文本。
assertEmailHtmlBodyContains(RawMessage $email, string $text, string $message = '')/assertEmailHtmlBodyNotContains(RawMessage $email, string $text, string $message = '')
断言给定电子邮件的 HTML 正文(不)包含预期的文本。
assertEmailHasHeader(RawMessage $email, string $headerName, string $message = '')/assertEmailNotHasHeader(RawMessage $email, string $headerName, string $message = '')
断言给定的电子邮件(不)具有设置的预期标头。
assertEmailHeaderSame(RawMessage $email, string $headerName, string $expectedValue, string $message = '')/assertEmailHeaderNotSame(RawMessage $email, string $headerName, string $expectedValue, string $message = '')
断言给定的电子邮件(不)具有设置为预期值的预期标头。
assertEmailAddressContains(RawMessage $email, string $headerName, string $expectedValue, string $message = '')
断言给定的地址标头等于预期的电子邮件地址。此断言将诸如 Jane Smith <jane@example.com> 之类的地址规范化为 jane@example.com
assertEmailSubjectContains(RawMessage $email, string $expectedValue, string $message = '')/assertEmailSubjectNotContains(RawMessage $email, string $expectedValue, string $message = '')
断言给定电子邮件的主题(不)包含预期的主题。

Notifier 断言

assertNotificationCount(int $count, ?string $transportName = null, string $message = '')
断言已创建的通知的给定数量(总数或给定传输方式)。
assertQueuedNotificationCount(int $count, ?string $transportName = null, string $message = '')
断言已排队的通知的给定数量(总数或给定传输方式)。
assertNotificationIsQueued(MessageEvent $event, string $message = '')
断言给定的通知已排队。
assertNotificationIsNotQueued(MessageEvent $event, string $message = '')
断言给定的通知未排队。
assertNotificationSubjectContains(MessageInterface $notification, string $text, string $message = '')
断言给定的文本包含在给定通知的主题中。
assertNotificationSubjectNotContains(MessageInterface $notification, string $text, string $message = '')
断言给定的文本不包含在给定通知的主题中。
assertNotificationTransportIsEqual(MessageInterface $notification, string $transportName, string $message = '')
断言给定通知的传输方式名称与给定的文本相同。
assertNotificationTransportIsNotEqual(MessageInterface $notification, string $transportName, string $message = '')
断言给定通知的传输方式名称与给定的文本不相同。

HttpClient 断言

提示

对于以下所有断言,必须在触发 HTTP 请求的代码之前调用 $client->enableProfiler()

assertHttpClientRequest(string $expectedUrl, string $expectedMethod = 'GET', string|array|null $expectedBody = null, array $expectedHeaders = [], string $httpClientId = 'http_client')
断言已使用给定的方法正文和标头调用给定的 URL(如果已指定)。默认情况下,它将检查 HttpClient,但您也可以传递特定的 HttpClient ID。(如果请求已被多次调用,它将成功。)
assertNotHttpClientRequest(string $unexpectedUrl, string $expectedMethod = 'GET', string $httpClientId = 'http_client')
断言未使用 GET 或指定的方法调用给定的 URL。默认情况下,它将检查 HttpClient,但可以指定 HttpClient ID。
assertHttpClientRequestCount(int $count, string $httpClientId = 'http_client')
断言已在 HttpClient 上发出给定数量的请求。默认情况下,它将检查 HttpClient,但您也可以传递特定的 HttpClient ID。

端到端测试 (E2E)

如果您需要作为一个整体测试应用程序,包括 JavaScript 代码,您可以使用真正的浏览器而不是测试客户端。这称为端到端测试,它是测试应用程序的好方法。

这可以通过 Panther 组件来实现。您可以在 专用页面 中了解更多信息。

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本