测试

CakePHP 具有内置的全面测试支持。CakePHP 集成了 PHPUnit。除了 PHPUnit 提供的功能外,CakePHP 还提供了一些额外的功能,使测试更容易。本节将介绍安装 PHPUnit 以及如何开始使用单元测试,以及如何使用 CakePHP 提供的扩展。

安装 PHPUnit

CakePHP 使用 PHPUnit 作为其底层测试框架。PHPUnit 是 PHP 中事实上的单元测试标准。它提供了一套强大而深入的功能,用于确保您的代码按预期执行。PHPUnit 可以通过使用 PHAR 包Composer 来安装。

使用 Composer 安装 PHPUnit

要使用 Composer 安装 PHPUnit

$ php composer.phar require --dev phpunit/phpunit:"^10.1"

这会将依赖项添加到您 composer.jsonrequire-dev 部分,然后安装 PHPUnit 以及任何依赖项。

您现在可以使用以下命令运行 PHPUnit

$ vendor/bin/phpunit

使用 PHAR 文件

下载 phpunit.phar 文件后,可以使用它来运行测试

php phpunit.phar

提示

为了方便起见,您可以在 Unix 或 Linux 上使用以下命令使 phpunit.phar 在全局范围内可用

chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
phpunit --version

有关在 Windows 上全局安装 PHPUnit PHAR 的说明,请参阅 PHPUnit 文档 https://phpunit.de/manual/current/en/installation.html#installation.phar.windows

测试数据库设置

请记住,在运行任何测试之前,您需要在 config/app_local.php 文件中启用调试。在运行任何测试之前,您应该确保在 config/app_local.php 中添加一个 test 数据源配置。CakePHP 使用此配置用于夹具表和数据

'Datasources' => [
    'test' => [
        'datasource' => 'Cake\Database\Driver\Mysql',
        'persistent' => false,
        'host' => 'dbhost',
        'username' => 'dblogin',
        'password' => 'dbpassword',
        'database' => 'test_database',
    ],
],

注意

最好让测试数据库与您的实际数据库不同。这将防止以后出现令人尴尬的错误。

检查测试设置

安装 PHPUnit 并设置 test 数据源配置后,您可以通过运行应用程序的测试来确保您已准备好编写和运行自己的测试

# For phpunit.phar
$ php phpunit.phar

# For Composer installed phpunit
$ vendor/bin/phpunit

以上命令将运行您拥有的任何测试,或者让您知道没有运行任何测试。要运行特定测试,您可以将测试路径作为参数提供给 PHPUnit。例如,如果您有一个针对 ArticlesTable 类的测试用例,您可以使用以下命令运行它

$ vendor/bin/phpunit tests/TestCase/Model/Table/ArticlesTableTest

您应该会看到一个绿色的条,其中包含有关运行的测试和通过数量的更多信息。

注意

如果您使用的是 Windows 系统,您可能不会看到任何颜色。

测试用例约定

与 CakePHP 中的大多数事物一样,测试用例也有一些约定。关于测试

  1. 包含测试的 PHP 文件应位于您的 tests/TestCase/[Type] 目录中。

  2. 这些文件的名称应以 Test.php 结尾,而不是 .php。

  3. 包含测试的类应扩展 Cake\TestSuite\TestCaseCake\TestSuite\IntegrationTestCase\PHPUnit\Framework\TestCase

  4. 与其他类名一样,测试用例类名应与文件名匹配。RouterTest.php 应包含 class RouterTest extends TestCase

  5. 包含测试的任何方法(即包含断言的方法)的名称应以 test 开头,例如 testPublished()。您还可以使用 @test 注释将方法标记为测试方法。

创建您的第一个测试用例

在以下示例中,我们将为一个非常简单的辅助方法创建一个测试用例。我们将测试的辅助方法将格式化进度条 HTML。我们的辅助方法如下所示

namespace App\View\Helper;

use Cake\View\Helper;

class ProgressHelper extends Helper
{
    public function bar($value)
    {
        $width = round($value / 100, 2) * 100;

        return sprintf(
            '<div class="progress-container">
                <div class="progress-bar" style="width: %s%%"></div>
            </div>', $width);
    }
}

这是一个非常简单的示例,但它将有助于说明如何创建一个简单的测试用例。在创建并保存我们的辅助方法后,我们将创建位于 tests/TestCase/View/Helper/ProgressHelperTest.php 中的测试用例文件。在该文件中,我们将从以下内容开始

namespace App\Test\TestCase\View\Helper;

use App\View\Helper\ProgressHelper;
use Cake\TestSuite\TestCase;
use Cake\View\View;

class ProgressHelperTest extends TestCase
{
    public function setUp(): void
    {
    }

    public function testBar(): void
    {
    }
}

我们将在稍后填充此骨架。我们已经添加了两种方法。第一个是 setUp()。此方法在测试用例类中的每个测试方法之前调用。设置方法应初始化测试所需的任何对象,并进行所需的任何配置。在我们的设置方法中,我们将添加以下内容

public function setUp(): void
{
    parent::setUp();
    $View = new View();
    $this->Progress = new ProgressHelper($View);
}

在测试用例中调用父方法很重要,因为 TestCase::setUp() 会执行一些操作,例如备份 Cake\Core\Configure 中的值,以及存储 Cake\Core\App 中的路径。

接下来,我们将填充测试方法。我们将使用一些断言来确保我们的代码创建了我们期望的输出

public function testBar(): void
{
    $result = $this->Progress->bar(90);
    $this->assertStringContainsString('width: 90%', $result);
    $this->assertStringContainsString('progress-bar', $result);

    $result = $this->Progress->bar(33.3333333);
    $this->assertStringContainsString('width: 33%', $result);
}

以上测试很简单,但展示了使用测试用例的潜在好处。我们使用 assertStringContainsString() 来确保我们的辅助方法返回的字符串包含我们期望的内容。如果结果不包含预期内容,则测试将失败,我们将知道我们的代码不正确。

通过使用测试用例,您可以描述一组已知输入及其预期输出之间的关系。这有助于您对正在编写的代码更有信心,因为您可以确保您编写的代码满足测试所做的期望和断言。此外,由于测试是代码,因此在您进行任何更改时都可以重新运行它们。这有助于防止出现新的错误。

注意

EventManager 在每个测试方法中都刷新。这意味着,当一次运行多个测试时,您将丢失在 config/bootstrap.php 中注册的事件监听器,因为 bootstrap 只执行一次。

运行测试

安装 PHPUnit 并编写了一些测试用例后,您需要频繁地运行测试用例。最好在提交任何更改之前运行测试,以帮助确保您没有破坏任何东西。

通过使用 phpunit,您可以运行您的应用程序测试。要运行您的应用程序的测试,您只需运行

vendor/bin/phpunit

php phpunit.phar

如果您从 GitHub 克隆了 CakePHP 源代码 并希望运行 CakePHP 的单元测试,请不要忘记在运行 phpunit 之前执行以下 Composer 命令,以便安装任何依赖项

composer install

从您的应用程序的根目录开始。要运行应用程序源代码中包含的插件的测试,首先 cd 到插件目录,然后使用与您安装 phpunit 方式相匹配的 phpunit 命令

cd plugins

../vendor/bin/phpunit

php ../phpunit.phar

要在独立插件上运行测试,您应该首先在单独的目录中安装项目并安装其依赖项。

git clone git://github.com/cakephp/debug_kit.git
cd debug_kit
php ~/composer.phar install
php ~/phpunit.phar

过滤测试用例

当您有更大的测试用例时,您通常希望在尝试解决单个失败案例时运行测试方法的子集。使用 CLI 运行器,您可以使用一个选项来过滤测试方法。

$ phpunit --filter testSave tests/TestCase/Model/Table/ArticlesTableTest

filter 参数用作区分大小写的正则表达式,用于过滤要运行的测试方法。

生成代码覆盖率

您可以使用 PHPUnit 的内置代码覆盖率工具从命令行生成代码覆盖率报告。PHPUnit 将生成一组包含覆盖率结果的静态 HTML 文件。您可以通过以下步骤为测试用例生成覆盖率:

$ phpunit --coverage-html webroot/coverage tests/TestCase/Model/Table/ArticlesTableTest

这会将覆盖率结果放到应用程序的 webroot 目录中。您应该可以通过访问 https://127.0.0.1/your_app/coverage 来查看结果。

您也可以使用 phpdbg 来生成覆盖率,而不是 xdebug。 phpdbg 通常在生成覆盖率方面更快。

$ phpdbg -qrr phpunit --coverage-html webroot/coverage tests/TestCase/Model/Table/ArticlesTableTest

合并插件的测试套件

通常情况下,您的应用程序将由多个插件组成。在这种情况下,为每个插件运行测试可能非常繁琐。您可以通过向应用程序的 phpunit.xml.dist 文件中添加额外的 <testsuite> 部分来运行构成应用程序的每个插件的测试。

<testsuites>
    <testsuite name="app">
        <directory>tests/TestCase/</directory>
    </testsuite>

    <!-- Add your plugin suites -->
    <testsuite name="forum">
        <directory>plugins/Forum/tests/TestCase/</directory>
    </testsuite>
</testsuites>

当您使用 phpunit 时,添加到 <testsuites> 元素中的任何额外测试套件都将自动运行。

如果您使用 <testsuites> 来使用通过 composer 安装的插件中的 fixture,则插件的 composer.json 文件应该将 fixture 命名空间添加到 autoload 部分。例如

"autoload-dev": {
    "psr-4": {
        "PluginName\\Test\\Fixture\\": "tests/Fixture/"
    }
},

测试用例生命周期回调

测试用例具有一系列生命周期回调,您可以在测试时使用这些回调。

  • setUp 在每个测试方法之前调用。应该用于创建要测试的对象,并初始化测试的任何数据。始终记得调用 parent::setUp()

  • tearDown 在每个测试方法之后调用。应该用于在测试完成后清理。始终记得调用 parent::tearDown()

  • setupBeforeClass 在案例中的测试方法开始之前调用一次。此方法必须是静态的。

  • tearDownAfterClass 在案例中的测试方法开始之后调用一次。此方法必须是静态的。

Fixtures

在测试依赖于模型和数据库的代码时,可以使用fixture 作为为应用程序的测试创建初始状态的一种方式。通过使用 fixture 数据,您可以减少测试中的重复设置步骤。fixture 非常适合在许多或所有测试中共享的通用数据。仅在测试子集中需要的应该在测试中根据需要创建。

CakePHP 使用您的 config/app.php 配置文件中名为 test 的连接。如果此连接不可用,将引发异常,您将无法使用数据库 fixture。

CakePHP 在测试运行过程中执行以下操作:

  1. 为每个需要的 fixture 创建表。

  2. 使用数据填充表。

  3. 运行测试方法。

  4. 清空 fixture 表。

fixture 的模式是在测试运行开始时通过迁移或 SQL 倾倒文件创建的。

测试连接

默认情况下,CakePHP 将为应用程序中的每个连接创建别名。在应用程序的引导程序中定义的每个不以 test_ 开头的连接都将创建一个以 test_ 为前缀的别名。为连接创建别名可以确保您不会在测试用例中意外使用错误的连接。连接别名对应用程序的其余部分是透明的。例如,如果您使用“default”连接,则在测试用例中将获得 test 连接。如果您使用“replica”连接,则测试套件将尝试使用“test_replica”。

PHPUnit 配置

在使用 fixture 之前,您应该仔细检查您的 phpunit.xml 是否包含 fixture 扩展。

<!-- in phpunit.xml -->
<!-- Setup the extension for fixtures -->
<extensions>
    <bootstrap class="Cake\TestSuite\Fixture\Extension\PHPUnitExtension"/>
</extensions>

默认情况下,此扩展包含在您的应用程序和由 bake 生成的插件中。

在测试中创建模式

您可以通过 CakePHP 的迁移、加载 SQL 倾倒文件或使用其他外部模式管理工具来生成测试数据库模式。您应该在应用程序的 tests/bootstrap.php 文件中创建模式。

使用迁移创建模式

如果您使用 CakePHP 的 migrations 插件 来管理应用程序的模式,您也可以重复使用这些迁移来生成测试数据库模式。

// in tests/bootstrap.php
use Migrations\TestSuite\Migrator;

$migrator = new Migrator();

// Simple setup for with no plugins
$migrator->run();

// Run migrations for a plugin
$migrator->run(['plugin' => 'Contacts']);

// Run the Documents migrations on the test_docs connection.
$migrator->run(['plugin' => 'Documents', 'connection' => 'test_docs']);

如果您需要运行多组迁移,则可以按如下方式运行它们。

$migrator->runMany([
    // Run app migrations on test connection.
    ['connection' => 'test'],
    // Run Contacts migrations on test connection.
    ['plugin' => 'Contacts'],
    // Run Documents migrations on test_docs connection.
    ['plugin' => 'Documents', 'connection' => 'test_docs']
]);

使用 runMany() 将确保共享数据库的插件在运行每组迁移时不会删除表。

迁移插件只会运行未应用的迁移,如果当前迁移头与已应用的迁移不同,则会重置迁移。

您还可以在数据源配置中配置迁移在测试中的运行方式。有关详细信息,请参阅 迁移文档

使用抽象模式创建模式

对于需要在测试中定义模式但不需要或不想依赖迁移的插件,您可以将模式定义为表的结构化数组。此格式不建议用于应用程序开发,因为它可能很耗时,需要维护。

每个表都可以定义 columnsconstraintsindexes。一个示例表将是

 return [
   'articles' => [
      'columns' => [
          'id' => [
              'type' => 'integer',
          ],
          'author_id' => [
              'type' => 'integer',
              'null' => true,
          ],
          'title' => [
              'type' => 'string',
              'null' => true,
          ],
          'body' => 'text',
          'published' => [
              'type' => 'string',
              'length' => 1,
              'default' => 'N',
          ],
      ],
      'constraints' => [
          'primary' => [
              'type' => 'primary',
              'columns' => [
                  'id',
              ],
          ],
      ],
   ],
   // More tables.
];

可用于 columnsindexesconstraints 的选项与 CakePHP 的模式反射 API 中可用的属性匹配。表是增量创建的,您必须注意确保在创建外键引用之前创建表。创建模式文件后,可以使用以下代码在 tests/bootstrap.php 中加载它。

$loader = new SchemaLoader();
$loader->loadInternalFile($pathToSchemaFile);

使用 SQL 倾倒文件创建模式

要加载 SQL 倾倒文件,可以使用以下代码:

// in tests/bootstrap.php
use Cake\TestSuite\Fixture\SchemaLoader;

// Load one or more SQL files.
(new SchemaLoader())->loadSqlFiles('path/to/schema.sql', 'test');

在每次测试运行开始时,SchemaLoader 将删除连接中的所有表,并根据提供的模式文件重建表。

Fixture 状态管理器

默认情况下,CakePHP 通过截断数据库中的所有表来重置每个测试结束时的 fixture 状态。随着应用程序的增长,此操作可能变得很昂贵。通过使用 TransactionStrategy,每个测试方法将在事务中运行,该事务在测试结束时回滚。这可以提高性能,但要求您的测试不要过度依赖静态 fixture 数据,因为自动增量值不会在每个测试之前重置。

可以在测试用例中定义 fixture 状态管理策略。

use Cake\TestSuite\TestCase;
use Cake\TestSuite\Fixture\FixtureStrategyInterface;
use Cake\TestSuite\Fixture\TransactionStrategy;

class ArticlesTableTest extends TestCase
{
    /**
     * Create the fixtures strategy used for this test case.
     * You can use a base class/trait to change multiple classes.
     */
    protected function getFixtureStrategy(): FixtureStrategyInterface
    {
        return new TransactionStrategy();
    }
}

创建 Fixtures

Fixtures 定义了将在每次测试开始时插入测试数据库中的记录。让我们创建第一个 fixture,它将用于测试我们自己的 Article 模型。在您的 tests/Fixture 目录中创建一个名为 ArticlesFixture.php 的文件,内容如下:

namespace App\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;

class ArticlesFixture extends TestFixture
{
      // Optional. Set this property to load fixtures to a different test datasource
      public $connection = 'test';

      public $records = [
          [
              'title' => 'First Article',
              'body' => 'First Article Body',
              'published' => '1',
              'created' => '2007-03-18 10:39:23',
              'modified' => '2007-03-18 10:41:31'
          ],
          [
              'title' => 'Second Article',
              'body' => 'Second Article Body',
              'published' => '1',
              'created' => '2007-03-18 10:41:23',
              'modified' => '2007-03-18 10:43:31'
          ],
          [
              'title' => 'Third Article',
              'body' => 'Third Article Body',
              'published' => '1',
              'created' => '2007-03-18 10:43:23',
              'modified' => '2007-03-18 10:45:31'
          ]
      ];
 }

注意

建议不要手动向自动增量列添加值,因为它会干扰 PostgreSQL 和 SQLServer 中的序列生成。

$connection 属性定义了 fixture 将使用的数据库源。如果您的应用程序使用多个数据源,您应该使 fixture 匹配模型的数据源,但以 test_ 为前缀。例如,如果您的模型使用 mydb 数据源,则您的 fixture 应该使用 test_mydb 数据源。如果 test_mydb 连接不存在,则您的模型将使用默认的 test 数据源。Fixture 数据源必须以 test 为前缀,以降低在运行测试时意外截断所有应用程序数据的可能性。

我们可以定义一组记录,这些记录将在创建 fixture 表后进行填充。格式非常简单,$records 是一个记录数组。$records 中的每个项目都应该是单行。在每行中,应该是一个关联数组,其中包含行的列和值。请记住,$records 数组中的每个记录必须具有相同的键,因为行是批量插入的。

动态数据

要在 fixture 记录中使用函数或其他动态数据,可以在 fixture 的 init() 方法中定义您的记录。

namespace App\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;

class ArticlesFixture extends TestFixture
{
    public function init(): void
    {
        $this->records = [
            [
                'title' => 'First Article',
                'body' => 'First Article Body',
                'published' => '1',
                'created' => date('Y-m-d H:i:s'),
                'modified' => date('Y-m-d H:i:s'),
            ],
        ];
        parent::init();
    }
}

注意

在覆盖 init() 时,请记住始终调用 parent::init()

在测试用例中加载 Fixtures

创建 fixture 后,您需要在测试用例中使用它们。在每个测试用例中,您应该加载所需的 fixture。您应该为每个将对其运行查询的模型加载一个 fixture。要加载 fixture,您需要在模型中定义 $fixtures 属性。

class ArticlesTest extends TestCase
{
    protected $fixtures = ['app.Articles', 'app.Comments'];
}

从 4.1.0 开始,您可以使用 getFixtures() 来使用方法定义 fixture 列表。

public function getFixtures(): array
{
    return [
        'app.Articles',
        'app.Comments',
    ];
}

以上将从应用程序的 Fixture 目录加载 Article 和 Comment 固定数据。您也可以从 CakePHP 核心或插件加载固定数据。

class ArticlesTest extends TestCase
{
    protected $fixtures = [
        'plugin.DebugKit.Articles',
        'plugin.MyVendorName/MyPlugin.Messages',
        'core.Comments',
    ];
}

使用 core 前缀将从 CakePHP 加载固定数据,而使用插件名称作为前缀将从指定的插件加载固定数据。

您可以在子目录中加载固定数据。如果您的应用程序较大,使用多个目录可以更轻松地组织您的固定数据。要加载子目录中的固定数据,只需在固定数据名称中包含子目录名称即可。

class ArticlesTest extends CakeTestCase
{
    protected $fixtures = ['app.Blog/Articles', 'app.Blog/Comments'];
}

在上面的示例中,两个固定数据都将从 tests/Fixture/Blog/ 加载。

固定数据工厂

随着您的应用程序的增长,您的测试固定数据的数量和大小也会随之增加。您可能会发现难以维护它们并跟踪其内容。固定数据工厂插件 为大型应用程序提供了一种替代方案。

该插件使用 测试套件精简插件 在每次测试之前截断所有脏表。

以下命令将帮助您烘焙工厂

bin/cake bake fixture_factory -h

一旦您的工厂 调整完毕,您就可以立即创建测试固定数据。

与数据库的无必要交互会减慢您的测试以及应用程序的速度。您可以创建测试固定数据而不将其持久化,这对于测试没有数据库交互的方法很有用。

$article = ArticleFactory::make()->getEntity();

为了持久化

$article = ArticleFactory::make()->persist();

工厂也有助于创建关联的固定数据。假设文章属于多个作者,我们现在可以例如创建 5 篇文章,每篇包含 2 个作者。

$articles = ArticleFactory::make(5)->with('Authors', 2)->getEntities();

请注意,固定数据工厂不需要任何固定数据创建或声明。尽管如此,它们与 CakePHP 附带的固定数据完全兼容。您可以在 此处 找到更多见解和文档。

在测试中加载路由

如果您正在测试邮件程序、控制器组件或其他需要路由和解析 URL 的类,则需要加载路由。在类的 setUp() 中或在单个测试方法期间,您可以使用 loadRoutes() 来确保您的应用程序路由已加载。

public function setUp(): void
{
    parent::setUp();
    $this->loadRoutes();
}

此方法将构建您的 Application 的实例,并对其调用 routes() 方法。如果您的 Application 类需要专门的构造函数参数,您可以将其提供给 loadRoutes($constructorArgs)

在测试中创建路由

有时可能需要在测试中动态添加路由,例如在开发插件或可扩展的应用程序时。

就像加载现有的应用程序路由一样,这可以在测试方法的 setup() 中完成,也可以在单个测试方法本身中完成。

use Cake\Routing\Route\DashedRoute;
use Cake\Routing\RouteBuilder;
use Cake\Routing\Router;
use Cake\TestSuite\TestCase;

class PluginHelperTest extends TestCase
{
    protected RouteBuilder $routeBuilder;

    public function setUp(): void
    {
        parent::setUp();

        $this->routeBuilder = Router::createRouteBuilder('/');
        $this->routeBuilder->scope('/', function (RouteBuilder $routes) {
            $routes->setRouteClass(DashedRoute::class);
            $routes->get(
                '/test/view/{id}',
                ['controller' => 'Tests', 'action' => 'view']
            );
            // ...
        });

        // ...
    }
}

这将创建一个新的路由构建器实例,该实例将合并连接的路由到与所有其他可能已经存在或将在环境中创建的路由构建器实例使用的相同路由集合中。

在测试中加载插件

如果您的应用程序会动态加载插件,则可以使用 loadPlugins() 在测试期间加载一个或多个插件。

public function testMethodUsingPluginResources()
{
    $this->loadPlugins(['Company/Cms']);
    // Test logic that requires Company/Cms to be loaded.
}

测试表类

假设我们已经在 src/Model/Table/ArticlesTable.php 中定义了 Articles 表类,并且它看起来像这样

namespace App\Model\Table;

use Cake\ORM\Table;
use Cake\ORM\Query\SelectQuery;

class ArticlesTable extends Table
{
    public function findPublished(SelectQuery $query): SelectQuery
    {
        $query->where([
            $this->getAlias() . '.published' => 1
        ]);

        return $query;
    }
}

我们现在想设置一个测试来测试这个表类。现在让我们在您的 tests/TestCase/Model/Table 目录中创建一个名为 ArticlesTableTest.php 的文件,其内容如下

namespace App\Test\TestCase\Model\Table;

use App\Model\Table\ArticlesTable;
use Cake\TestSuite\TestCase;

class ArticlesTableTest extends TestCase
{
    protected $fixtures = ['app.Articles'];
}

在我们的测试用例的变量 $fixtures 中,我们定义了将要使用的固定数据集合。您应该记住要包含将对它们运行查询的所有固定数据。

创建测试方法

现在让我们添加一个方法来测试 Articles 表中 published() 函数。编辑 tests/TestCase/Model/Table/ArticlesTableTest.php 文件,使其现在看起来像这样

namespace App\Test\TestCase\Model\Table;

use App\Model\Table\ArticlesTable;
use Cake\TestSuite\TestCase;

class ArticlesTableTest extends TestCase
{
    protected $fixtures = ['app.Articles'];

    public function setUp(): void
    {
        parent::setUp();
        $this->Articles = $this->getTableLocator()->get('Articles');
    }

    public function testFindPublished(): void
    {
        $query = $this->Articles->find('published')->select(['id', 'title']);
        $this->assertInstanceOf('Cake\ORM\Query\SelectQuery', $query);
        $result = $query->enableHydration(false)->toArray();
        $expected = [
            ['id' => 1, 'title' => 'First Article'],
            ['id' => 2, 'title' => 'Second Article'],
            ['id' => 3, 'title' => 'Third Article']
        ];

        $this->assertEquals($expected, $result);
    }
}

您可以看到我们添加了一个名为 testFindPublished() 的方法。我们首先创建我们的 ArticlesTable 类的实例,然后运行我们的 find('published') 方法。在 $expected 中,我们设置了我们期望应该得到的结果(我们知道,因为我们已经定义了最初填充到文章表的记录。)我们通过使用 assertEquals() 方法测试结果是否等于我们的期望。有关如何运行测试用例的更多信息,请参见 运行测试 部分。

使用固定数据工厂,测试现在将如下所示

namespace App\Test\TestCase\Model\Table;

use App\Test\Factory\ArticleFactory;
use Cake\TestSuite\TestCase;

class ArticlesTableTest extends TestCase
{
    public function testFindPublished(): void
    {
        // Persist 3 published articles
        $articles = ArticleFactory::make(['published' => 1], 3)->persist();
        // Persist 2 unpublished articles
        ArticleFactory::make(['published' => 0], 2)->persist();

        $result = ArticleFactory::find('published')->find('list')->toArray();

        $expected = [
            $articles[0]->id => $articles[0]->title,
            $articles[1]->id => $articles[1]->title,
            $articles[2]->id => $articles[2]->title,
        ];

        $this->assertEquals($expected, $result);
    }
}

不需要加载固定数据。创建的 5 篇文章只存在于此测试中。静态方法 ::find() 将查询数据库而不使用表 ArticlesTable 及其事件。

模拟模型方法

在测试模型时,有时您需要模拟模型上的方法。您应该使用 getMockForModel 创建表类的测试模拟。它避免了普通模拟所具有的反射属性问题。

public function testSendingEmails(): void
{
    $model = $this->getMockForModel('EmailVerification', ['send']);
    $model->expects($this->once())
        ->method('send')
        ->will($this->returnValue(true));

    $model->verifyEmail('[email protected]');
}

在您的 tearDown() 方法中,请确保使用以下命令删除模拟

$this->getTableLocator()->clear();

控制器集成测试

虽然您可以像测试助手、模型和组件一样测试控制器类,但 CakePHP 提供了一个专门的 IntegrationTestTrait 特性。在您的控制器测试用例中使用此特性允许您从高级别测试控制器。

如果您不熟悉集成测试,它是一种测试方法,允许您协同测试多个单元。CakePHP 中的集成测试功能模拟了 HTTP 请求由您的应用程序处理的过程。例如,测试您的控制器还将执行处理给定请求时涉及的任何组件、模型和助手。这为您提供了对应用程序及其所有工作部分的更高级别的测试。

假设您有一个典型的 ArticlesController,以及与其对应的模型。控制器代码如下所示

namespace App\Controller;

use App\Controller\AppController;

class ArticlesController extends AppController
{
    public function index($short = null)
    {
        if ($this->request->is('post')) {
            $article = $this->Articles->newEntity($this->request->getData());
            if ($this->Articles->save($article)) {
                // Redirect as per PRG pattern
                return $this->redirect(['action' => 'index']);
            }
        }
        if (!empty($short)) {
            $result = $this->Articles->find('all', fields: ['id', 'title'])->all();
        } else {
            $result = $this->Articles->find()->all();
        }

        $this->set([
            'title' => 'Articles',
            'articles' => $result
        ]);
    }
}

在您的 tests/TestCase/Controller 目录中创建一个名为 ArticlesControllerTest.php 的文件,并将以下内容放在其中

namespace App\Test\TestCase\Controller;

use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;

class ArticlesControllerTest extends TestCase
{
    use IntegrationTestTrait;

    protected $fixtures = ['app.Articles'];

    public function testIndex(): void
    {
        $this->get('/articles');

        $this->assertResponseOk();
        // More asserts.
    }

    public function testIndexQueryData(): void
    {
        $this->get('/articles?page=1');

        $this->assertResponseOk();
        // More asserts.
    }

    public function testIndexShort(): void
    {
        $this->get('/articles/index/short');

        $this->assertResponseOk();
        $this->assertResponseContains('Articles');
        // More asserts.
    }

    public function testIndexPostData(): void
    {
        $data = [
            'user_id' => 1,
            'published' => 1,
            'slug' => 'new-article',
            'title' => 'New Article',
            'body' => 'New Body'
        ];
        $this->post('/articles', $data);

        $this->assertResponseSuccess();
        $articles = $this->getTableLocator()->get('Articles');
        $query = $articles->find()->where(['title' => $data['title']]);
        $this->assertEquals(1, $query->count());
    }
}

此示例展示了一些请求发送方法和 IntegrationTestTrait 提供的一些断言。在进行任何断言之前,您需要调度一个请求。您可以使用以下方法之一发送请求

  • get() 发送 GET 请求。

  • post() 发送 POST 请求。

  • put() 发送 PUT 请求。

  • delete() 发送 DELETE 请求。

  • patch() 发送 PATCH 请求。

  • options() 发送 OPTIONS 请求。

  • head() 发送 HEAD 请求。

除了 get()delete() 之外,所有方法都接受第二个参数,允许您发送请求主体。调度请求后,您可以使用 IntegrationTestTrait 或 PHPUnit 提供的各种断言来确保您的请求产生了正确的副作用。

设置请求

IntegrationTestTrait 特性附带了许多助手来配置您将发送到正在测试的应用程序的请求。

// Set cookies
$this->cookie('name', 'Uncle Bob');

// Set session data
$this->session(['Auth.User.id' => 1]);

// Configure headers and merge with the existing request
$this->configRequest([
    'headers' => ['Accept' => 'application/json']
]);

// Replace the existing request. Added in 5.1.0
$this->replaceRequest([
    'headers' => ['Accept' => 'application/json']
]);

tearDown() 方法中重置这些助手方法设置的状态。

在版本 5.1.0 中添加: replaceRequest() 已添加。

测试受 CsrfProtectionMiddleware 或 FormProtectionComponent 保护的操作

当测试受 CsrfProtectionMiddlewareFormProtectionComponent 保护的操作时,您可以启用自动令牌生成,以确保您的测试不会因令牌不匹配而失败。

public function testAdd(): void
{
    $this->enableCsrfToken();
    $this->enableSecurityToken();
    $this->post('/posts/add', ['title' => 'Exciting news!']);
}

在使用令牌的测试中启用调试也很重要,以防止 FormProtectionComponent 认为调试令牌正在非调试环境中使用。当使用 requireSecure() 等其他方法进行测试时,您可以使用 configRequest() 来设置正确的环境变量。

// Fake out SSL connections.
$this->configRequest([
    'environment' => ['HTTPS' => 'on']
]);

如果您的操作需要解锁字段,您可以使用 setUnlockedFields() 声明它们。

$this->setUnlockedFields(['dynamic_field']);

集成测试 PSR-7 中间件

集成测试也可以用于测试您的整个 PSR-7 应用程序和 中间件。默认情况下,IntegrationTestTrait 将自动检测 App\Application 类的存在,并自动启用应用程序的集成测试。

您可以通过使用 configApplication() 方法来自定义使用的应用程序类名和构造函数参数。

public function setUp(): void
{
    $this->configApplication('App\App', [CONFIG]);
}

您还应该注意尝试使用 Application::bootstrap() 加载包含事件/路由的任何插件。这样做将确保为每个测试用例连接您的事件/路由。或者,如果您希望在测试中手动加载插件,可以使用 loadPlugins() 方法。

使用加密 Cookie 进行测试

如果您在应用程序中使用 加密 Cookie 中间件,则有一些助手方法可以在测试用例中设置加密 Cookie。

// Set a cookie using AES and the default key.
$this->cookieEncrypted('my_cookie', 'Some secret values');

// Assume this action modifies the cookie.
$this->get('/articles/index');

$this->assertCookieEncrypted('An updated value', 'my_cookie');

测试闪存消息

如果您希望在会话中而不是渲染的HTML中断言闪存消息的存在,您可以在测试中使用 enableRetainFlashMessages() 来保留会话中的闪存消息,以便您可以编写断言

// Enable retention of flash messages instead of consuming them.
$this->enableRetainFlashMessages();
$this->get('/articles/delete/9999');

$this->assertSession('That article does not exist', 'Flash.flash.0.message');

// Assert a flash message in the 'flash' key.
$this->assertFlashMessage('Article deleted', 'flash');

// Assert the second flash message, also  in the 'flash' key.
$this->assertFlashMessageAt(1, 'Article really deleted');

// Assert a flash message in the 'auth' key at the first position
$this->assertFlashMessageAt(0, 'You are not allowed to enter this dungeon!', 'auth');

// Assert a flash messages uses the error element
$this->assertFlashElement('Flash/error');

// Assert the second flash message element
$this->assertFlashElementAt(1, 'Flash/error');

测试 JSON 响应控制器

JSON 是一种在构建 Web 服务时使用起来友好且常见的格式。使用 CakePHP 测试 Web 服务的端点非常简单。让我们从一个简单的示例控制器开始,该控制器以 JSON 响应

use Cake\View\JsonView;

class MarkersController extends AppController
{
    public function viewClasses(): array
    {
        return [JsonView::class];
    }

    public function view($id)
    {
        $marker = $this->Markers->get($id);
        $this->set('marker', $marker);
        $this->viewBuilder()->setOption('serialize', ['marker']);
    }
}

现在我们创建文件 tests/TestCase/Controller/MarkersControllerTest.php 并确保我们的 Web 服务正在返回正确的响应

class MarkersControllerTest extends IntegrationTestCase
{
    use IntegrationTestTrait;

    public function testGet(): void
    {
        $this->configRequest([
            'headers' => ['Accept' => 'application/json']
        ]);
        $this->get('/markers/view/1.json');

        // Check that the response was a 200
        $this->assertResponseOk();

        $expected = [
            ['id' => 1, 'lng' => 66, 'lat' => 45],
        ];
        $expected = json_encode($expected, JSON_PRETTY_PRINT);
        $this->assertEquals($expected, (string)$this->_response->getBody());
    }
}

我们使用 JSON_PRETTY_PRINT 选项,因为 CakePHP 的内置 JsonView 在 debug 启用时会使用该选项。

使用文件上传进行测试

当您使用默认的“上传的文件作为对象”模式时,模拟文件上传很简单。您只需创建实现 \Psr\Http\Message\UploadedFileInterface 的实例(CakePHP 目前使用的默认实现是 \Laminas\Diactoros\UploadedFile),并将它们传递到您的测试请求数据中。在 CLI 环境中,此类对象默认情况下将通过验证检查,这些检查将测试文件是否通过 HTTP 上传。对于 $_FILES 中发现的数组样式数据,情况并非如此,它将无法通过该检查。

为了准确模拟上传文件对象在常规请求中的出现方式,您不仅需要将它们传递到请求数据中,还需要通过 files 选项将它们传递到测试请求配置中。不过,除非您的代码通过 Cake\Http\ServerRequest::getUploadedFile()Cake\Http\ServerRequest::getUploadedFiles() 方法访问上传的文件,否则从技术上讲这不是必需的。

假设文章有一个预告图像,并且有一个 Articles hasMany Attachments 关联,那么表格将根据以下内容显示,其中将接受一个图像文件和多个附件/文件

<?= $this->Form->create($article, ['type' => 'file']) ?>
<?= $this->Form->control('title') ?>
<?= $this->Form->control('teaser_image', ['type' => 'file']) ?>
<?= $this->Form->control('attachments.0.attachment', ['type' => 'file']) ?>
<?= $this->Form->control('attachments.0.description']) ?>
<?= $this->Form->control('attachments.1.attachment', ['type' => 'file']) ?>
<?= $this->Form->control('attachments.1.description']) ?>
<?= $this->Form->button('Submit') ?>
<?= $this->Form->end() ?>

模拟相应请求的测试可能如下所示

public function testAddWithUploads(): void
{
    $teaserImage = new \Laminas\Diactoros\UploadedFile(
        '/path/to/test/file.jpg', // stream or path to file representing the temp file
        12345,                    // the filesize in bytes
        \UPLOAD_ERR_OK,           // the upload/error status
        'teaser.jpg',             // the filename as sent by the client
        'image/jpeg'              // the mimetype as sent by the client
    );

    $textAttachment = new \Laminas\Diactoros\UploadedFile(
        '/path/to/test/file.txt',
        12345,
        \UPLOAD_ERR_OK,
        'attachment.txt',
        'text/plain'
    );

    $pdfAttachment = new \Laminas\Diactoros\UploadedFile(
        '/path/to/test/file.pdf',
        12345,
        \UPLOAD_ERR_OK,
        'attachment.pdf',
        'application/pdf'
    );

    // This is the data accessible via `$this->request->getUploadedFile()`
    // and `$this->request->getUploadedFiles()`.
    $this->configRequest([
        'files' => [
            'teaser_image' => $teaserImage,
            'attachments' => [
                0 => [
                    'attachment' => $textAttachment,
                ],
                1 => [
                    'attachment' => $pdfAttachment,
                ],
            ],
        ],
    ]);

    // This is the data accessible via `$this->request->getData()`.
    $postData = [
        'title' => 'New Article',
        'teaser_image' => $teaserImage,
        'attachments' => [
            0 => [
                'attachment' => $textAttachment,
                'description' => 'Text attachment',
            ],
            1 => [
                'attachment' => $pdfAttachment,
                'description' => 'PDF attachment',
            ],
        ],
    ];
    $this->post('/articles/add', $postData);

    $this->assertResponseOk();
    $this->assertFlashMessage('The article was saved successfully');
    $this->assertFileExists('/path/to/uploads/teaser.jpg');
    $this->assertFileExists('/path/to/uploads/attachment.txt');
    $this->assertFileExists('/path/to/uploads/attachment.pdf');
}

提示

如果您使用文件配置测试请求,那么它*必须*与您的 POST 数据的结构匹配(但只包括上传的文件对象)!

同样,您可以模拟 上传错误 或其他无法通过验证的无效文件

public function testAddWithInvalidUploads(): void
{
    $missingTeaserImageUpload = new \Laminas\Diactoros\UploadedFile(
        '',
        0,
        \UPLOAD_ERR_NO_FILE,
        '',
        ''
    );

    $uploadFailureAttachment = new \Laminas\Diactoros\UploadedFile(
        '/path/to/test/file.txt',
        1234567890,
        \UPLOAD_ERR_INI_SIZE,
        'attachment.txt',
        'text/plain'
    );

    $invalidTypeAttachment = new \Laminas\Diactoros\UploadedFile(
        '/path/to/test/file.exe',
        12345,
        \UPLOAD_ERR_OK,
        'attachment.exe',
        'application/vnd.microsoft.portable-executable'
    );

    $this->configRequest([
        'files' => [
            'teaser_image' => $missingTeaserImageUpload,
            'attachments' => [
                0 => [
                    'file' => $uploadFailureAttachment,
                ],
                1 => [
                    'file' => $invalidTypeAttachment,
                ],
            ],
        ],
    ]);

    $postData = [
        'title' => 'New Article',
        'teaser_image' => $missingTeaserImageUpload,
        'attachments' => [
            0 => [
                'file' => $uploadFailureAttachment,
                'description' => 'Upload failure attachment',
            ],
            1 => [
                'file' => $invalidTypeAttachment,
                'description' => 'Invalid type attachment',
            ],
        ],
    ];
    $this->post('/articles/add', $postData);

    $this->assertResponseOk();
    $this->assertFlashMessage('The article could not be saved');
    $this->assertResponseContains('A teaser image is required');
    $this->assertResponseContains('Max allowed filesize exceeded');
    $this->assertResponseContains('Unsupported file type');
    $this->assertFileNotExists('/path/to/uploads/teaser.jpg');
    $this->assertFileNotExists('/path/to/uploads/attachment.txt');
    $this->assertFileNotExists('/path/to/uploads/attachment.exe');
}

在测试中禁用错误处理中间件

在调试由于应用程序遇到错误而导致失败的测试时,暂时禁用错误处理中间件以允许底层错误冒泡可能会有所帮助。您可以使用 disableErrorHandlerMiddleware() 来执行此操作

public function testGetMissing(): void
{
    $this->disableErrorHandlerMiddleware();
    $this->get('/markers/not-there');
    $this->assertResponseCode(404);
}

在上面的示例中,测试将失败,并且会显示底层异常消息和堆栈跟踪,而不是检查渲染的错误页面。

断言方法

IntegrationTestTrait 特性提供了一些断言方法,这些方法使测试响应变得更加简单。一些示例是

// Check for a 2xx response code
$this->assertResponseOk();

// Check for a 2xx/3xx response code
$this->assertResponseSuccess();

// Check for a 4xx response code
$this->assertResponseError();

// Check for a 5xx response code
$this->assertResponseFailure();

// Check for a specific response code, for example, 200
$this->assertResponseCode(200);

// Check the Location header
$this->assertRedirect(['controller' => 'Articles', 'action' => 'index']);

// Check that no Location header has been set
$this->assertNoRedirect();

// Check a part of the Location header
$this->assertRedirectContains('/articles/edit/');

// Assert location header does not contain
$this->assertRedirectNotContains('/articles/edit/');

// Assert not empty response content
$this->assertResponseNotEmpty();

// Assert empty response content
$this->assertResponseEmpty();

// Assert response content
$this->assertResponseEquals('Yeah!');

// Assert response content doesn't equal
$this->assertResponseNotEquals('No!');

// Assert partial response content
$this->assertResponseContains('You won!');
$this->assertResponseNotContains('You lost!');

// Assert file sent back
$this->assertFileResponse('/absolute/path/to/file.ext');

// Assert layout
$this->assertLayout('default');

// Assert which template was rendered (if any)
$this->assertTemplate('index');

// Assert data in the session
$this->assertSession(1, 'Auth.User.id');

// Assert response header.
$this->assertHeader('Content-Type', 'application/json');
$this->assertHeaderContains('Content-Type', 'html');

// Assert content-type header doesn't contain xml
$this->assertHeaderNotContains('Content-Type', 'xml');

// Assert view variables
$user =  $this->viewVariable('user');
$this->assertEquals('jose', $user->username);

// Assert cookie values in the response
$this->assertCookie('1', 'thingid');

// Assert a cookie is or is not present
$this->assertCookieIsSet('remember_me');
$this->assertCookieNotSet('remember_me');

// Check the content type
$this->assertContentType('application/json');

除了上述断言方法之外,您还可以使用 TestSuite 中的所有断言以及 PHPUnit 中的断言。

将测试结果与文件进行比较

对于某些类型的测试,将测试结果与文件内容进行比较可能更容易 - 例如,在测试视图的渲染输出时。 StringCompareTrait 为此目的添加了一个简单的断言方法。

使用涉及使用该特性,设置比较基准路径并调用 assertSameAsFile

use Cake\TestSuite\StringCompareTrait;
use Cake\TestSuite\TestCase;

class SomeTest extends TestCase
{
    use StringCompareTrait;

    public function setUp(): void
    {
        $this->_compareBasePath = APP . 'tests' . DS . 'comparisons' . DS;
        parent::setUp();
    }

    public function testExample(): void
    {
        $result = ...;
        $this->assertSameAsFile('example.php', $result);
    }
}

上面的示例将 $result 与文件 APP/tests/comparisons/example.php 的内容进行比较。

提供了一种通过设置环境变量 UPDATE_TEST_COMPARISON_FILES 来写入/更新测试文件的机制,这将创建和/或更新测试比较文件,因为它们被引用

phpunit
...
FAILURES!
Tests: 6, Assertions: 7, Failures: 1

UPDATE_TEST_COMPARISON_FILES=1 phpunit
...
OK (6 tests, 7 assertions)

git status
...
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   tests/comparisons/example.php

控制台集成测试

有关如何测试控制台命令,请参阅 测试命令

模拟注入的依赖项

有关如何在集成测试中替换使用依赖项注入容器注入的服务,请参阅 在测试中模拟服务

模拟 HTTP 客户端响应

请参阅 测试 以了解如何创建对外部 API 的模拟响应。

测试视图

通常大多数应用程序不会直接测试其 HTML 代码。这样做通常会导致脆弱、难以维护的测试套件,这些测试套件容易出错。使用 IntegrationTestTrait 编写功能测试时,您可以通过将 return 选项设置为“view”来检查渲染的视图内容。虽然可以使用 IntegrationTestTrait 测试视图内容,但可以使用 Selenium webdriver 等工具实现更强大且可维护的集成/视图测试。

测试组件

假设我们在应用程序中有一个名为 PagematronComponent 的组件。此组件帮助我们在使用它的所有控制器中设置分页限制值。这是我们位于 src/Controller/Component/PagematronComponent.php 中的示例组件

class PagematronComponent extends Component
{
    public $controller = null;

    public function setController($controller)
    {
        $this->controller = $controller;
        // Make sure the controller is using pagination
        if (!isset($this->controller->paginate)) {
            $this->controller->paginate = [];
        }
    }

    public function startup(EventInterface $event)
    {
        $this->setController($event->getSubject());
    }

    public function adjust($length = 'short'): void
    {
        switch ($length) {
            case 'long':
                $this->controller->paginate['limit'] = 100;
            break;
            case 'medium':
                $this->controller->paginate['limit'] = 50;
            break;
            default:
                $this->controller->paginate['limit'] = 20;
            break;
        }
    }
}

现在,我们可以编写测试以确保我们的分页 limit 参数由组件中的 adjust() 方法正确设置。我们创建文件 tests/TestCase/Controller/Component/PagematronComponentTest.php

namespace App\Test\TestCase\Controller\Component;

use App\Controller\Component\PagematronComponent;
use Cake\Controller\Controller;
use Cake\Controller\ComponentRegistry;
use Cake\Event\Event;
use Cake\Http\ServerRequest;
use Cake\Http\Response;
use Cake\TestSuite\TestCase;

class PagematronComponentTest extends TestCase
{
    protected $component;
    protected $controller;

    public function setUp(): void
    {
        parent::setUp();
        // Setup our component and provide it a basic controller.
        // If your component relies on Application features, use AppController.
        $request = new ServerRequest();
        $response = new Response();
        $this->controller = new Controller($request);
        $registry = new ComponentRegistry($this->controller);

        $this->component = new PagematronComponent($registry);
        $event = new Event('Controller.startup', $this->controller);
        $this->component->startup($event);
    }

    public function testAdjust(): void
    {
        // Test our adjust method with different parameter settings
        $this->component->adjust();
        $this->assertEquals(20, $this->controller->paginate['limit']);

        $this->component->adjust('medium');
        $this->assertEquals(50, $this->controller->paginate['limit']);

        $this->component->adjust('long');
        $this->assertEquals(100, $this->controller->paginate['limit']);
    }

    public function tearDown(): void
    {
        parent::tearDown();
        // Clean up after we're done
        unset($this->component, $this->controller);
    }
}

测试助手

由于助手类中存在大量的逻辑,因此确保这些类由测试用例覆盖非常重要。

首先,我们创建一个示例助手来进行测试。 CurrencyRendererHelper 将帮助我们在视图中显示货币,为了简单起见,它只有一种方法 usd()

// src/View/Helper/CurrencyRendererHelper.php
namespace App\View\Helper;

use Cake\View\Helper;

class CurrencyRendererHelper extends Helper
{
    public function usd($amount): string
    {
        return 'USD ' . number_format($amount, 2, '.', ',');
    }
}

在这里,我们将小数位数设置为 2,小数分隔符设置为点,千位分隔符设置为逗号,并在格式化的数字前添加“USD”字符串。

现在我们创建我们的测试

// tests/TestCase/View/Helper/CurrencyRendererHelperTest.php

namespace App\Test\TestCase\View\Helper;

use App\View\Helper\CurrencyRendererHelper;
use Cake\TestSuite\TestCase;
use Cake\View\View;

class CurrencyRendererHelperTest extends TestCase
{
    public $helper = null;

    // Here we instantiate our helper
    public function setUp(): void
    {
        parent::setUp();
        $View = new View();
        $this->helper = new CurrencyRendererHelper($View);
    }

    // Testing the usd() function
    public function testUsd(): void
    {
        $this->assertEquals('USD 5.30', $this->helper->usd(5.30));

        // We should always have 2 decimal digits
        $this->assertEquals('USD 1.00', $this->helper->usd(1));
        $this->assertEquals('USD 2.05', $this->helper->usd(2.05));

        // Testing the thousands separator
        $this->assertEquals(
          'USD 12,000.70',
          $this->helper->usd(12000.70)
        );
    }
}

在这里,我们使用不同的参数调用 usd(),并告诉测试套件检查返回的值是否与预期值相等。

保存此内容并执行测试。您应该看到一条绿色条和一条消息,表明通过了 1 次并且有 4 个断言。

当您测试使用其他助手的助手时,请务必模拟 View 类的 loadHelpers 方法。

测试事件

事件系统 是解耦应用程序代码的好方法,但有时在测试时,您往往会在执行这些事件的测试用例中测试事件的结果。这是一种额外的耦合形式,可以通过使用 assertEventFiredassertEventFiredWith 来消除。

扩展订单示例,假设我们有以下表格

class OrdersTable extends Table
{
    public function place($order): bool
    {
        if ($this->save($order)) {
            // moved cart removal to CartsTable
            $event = new Event('Model.Order.afterPlace', $this, [
                'order' => $order
            ]);
            $this->getEventManager()->dispatch($event);

            return true;
        }

        return false;
    }
}

class CartsTable extends Table
{
    public function initialize()
    {
        // Models don't share the same event manager instance,
        // so we need to use the global instance to listen to
        // events from other models
        \Cake\Event\EventManager::instance()->on(
            'Model.Order.afterPlace',
            callable: [$this, 'removeFromCart']
        );
    }

    public function removeFromCart(EventInterface $event): void
    {
        $order = $event->getData('order');
        $this->delete($order->cart_id);
    }
}

注意

要断言事件已触发,您必须首先在要断言的事件管理器上启用 跟踪事件

要测试上面的 OrdersTable,我们在 setUp() 中启用跟踪,然后断言事件已触发,并断言 $order 实体已传递到事件数据中

namespace App\Test\TestCase\Model\Table;

use App\Model\Table\OrdersTable;
use Cake\Event\EventList;
use Cake\TestSuite\TestCase;

class OrdersTableTest extends TestCase
{
    protected $fixtures = ['app.Orders'];

    public function setUp(): void
    {
        parent::setUp();
        $this->Orders = $this->getTableLocator()->get('Orders');
        // enable event tracking
        $this->Orders->getEventManager()->setEventList(new EventList());
    }

    public function testPlace(): void
    {
        $order = new Order([
            'user_id' => 1,
            'item' => 'Cake',
            'quantity' => 42,
        ]);

        $this->assertTrue($this->Orders->place($order));

        $this->assertEventFired('Model.Order.afterPlace', $this->Orders->getEventManager());
        $this->assertEventFiredWith('Model.Order.afterPlace', 'order', $order, $this->Orders->getEventManager());
    }
}

默认情况下,全局 EventManager 用于断言,因此测试全局事件不需要传递事件管理器

$this->assertEventFired('My.Global.Event');
$this->assertEventFiredWith('My.Global.Event', 'user', 1);

测试电子邮件

有关测试电子邮件的信息,请参阅 测试邮件程序

测试日志记录

有关测试日志消息的信息,请参阅 测试日志

创建测试套件

如果您希望同时运行多个测试,则可以创建测试套件。测试套件由多个测试用例组成。您可以在应用程序的 phpunit.xml 文件中创建测试套件。一个简单的例子是

<testsuites>
  <testsuite name="Models">
    <directory>src/Model</directory>
    <file>src/Service/UserServiceTest.php</file>
    <exclude>src/Model/Cloud/ImagesTest.php</exclude>
  </testsuite>
</testsuites>

为插件创建测试

插件的测试是在插件文件夹内的自己的目录中创建的。

/src
/plugins
    /Blog
        /tests
            /TestCase
            /Fixture

它们的工作方式与普通测试相同,但您必须记住在导入类时使用插件的命名约定。这是本手册插件章节中 BlogPost 模型的测试用例示例。与其他测试的不同之处在于第一行,其中导入了“Blog.BlogPost”。您还需要使用 plugin.Blog.BlogPosts 为插件夹具添加前缀

namespace Blog\Test\TestCase\Model\Table;

use Blog\Model\Table\BlogPostsTable;
use Cake\TestSuite\TestCase;

class BlogPostsTableTest extends TestCase
{
    // Plugin fixtures located in /plugins/Blog/tests/Fixture/
    protected $fixtures = ['plugin.Blog.BlogPosts'];

    public function testSomething(): void
    {
        // Test something.
    }
}

如果您想在应用程序测试中使用插件夹具,您可以使用 plugin.pluginName.fixtureName 语法在 $fixtures 数组中引用它们。此外,如果您使用供应商插件名称或夹具目录,您可以使用以下语法:plugin.vendorName/pluginName.folderName/fixtureName

在使用夹具之前,您应该确保在您的 phpunit.xml 文件中配置了 夹具监听器。您还应该确保您的夹具是可以加载的。确保您的 **composer.json** 文件中包含以下内容

"autoload-dev": {
    "psr-4": {
        "MyPlugin\\Test\\": "plugins/MyPlugin/tests/"
    }
}

注意

请记住在添加新的自动加载映射时运行 composer.phar dumpautoload

使用 Bake 生成测试

如果您使用 bake 生成脚手架,它也会生成测试存根。如果您需要重新生成测试用例骨架,或者您想为编写的代码生成测试骨架,您可以使用 bake

bin/cake bake test <type> <name>

<type> 应该以下列之一:

  1. 实体

  2. 控制器

  3. 组件

  4. 行为

  5. 助手

  6. Shell

  7. 任务

  8. Shell 助手

  9. 单元格

  10. 表单

  11. 邮件发送器

  12. 命令

<name> 应该为要为其烘焙测试骨架的对象的名称。