Spiral 在设计时非常强调测试。它内置了对 PHPUnit 的支持,并且为您的应用程序预先配置了 phpunit.xml
文件。
Spiral 应用程序的默认目录结构包含一个 tests
目录,该目录包含两个子目录:Feature
和 Unit
。
Unit 测试旨在测试代码的小型、隔离的部分,通常侧重于单个方法。这些测试不会启动整个 Spiral 应用程序,因此无法访问数据库或其他框架服务。
另一方面,Feature 测试旨在测试更大范围的代码,包括多个对象之间的交互,甚至是完整的 HTTP 请求到 JSON 端点。这些测试提供了对应用程序更全面的覆盖,并让您更有信心它按预期运行。
Spiral 提供了 spiral/testing
软件包,以帮助开发人员进行应用程序测试。该软件包提供了各种辅助方法,可以简化编写 Spiral 应用程序测试的过程。
为了运行测试,您可能需要设置某些环境变量。一种方法是使用 phpunit.xml
文件。PHPUnit 测试框架使用此文件来配置测试环境。
以下是一个示例:
<phpunit>
// ...
<php>
<env name="APP_ENV" value="testing"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="MONOLOG_DEFAULT_CHANNEL" value="stderr"/>
<env name="CACHE_STORAGE" value="array"/>
<env name="TOKENIZER_CACHE_TARGETS" value="true" />
<env name="CYCLE_SCHEMA_CACHE" value="true" />
// ...
</php>
</phpunit>
警告 当您在 Docker 容器中运行测试时,Docker 中的设置比
phpunit.xml
中的设置更重要。如果它们不匹配,这可能会导致问题。请确保 Docker 设置适合您的测试。
注意 强烈建议启用
TOKENIZER_CACHE_TARGETS
和CYCLE_SCHEMA_CACHE
以增强测试性能。通过这样做,您可以缓存 tokenizer 和 ORM schema,这意味着它们不会在每次测试迭代中执行。但是,请注意,每当您更改代码或实体的 schema 时,务必清除此缓存,以确保测试使用最新的配置运行。
另一方面,单元测试侧重于代码的小型、隔离的部分,应该扩展 PHPUnit\Framework\TestCase
类。单元测试不会启动整个 Spiral 应用程序,因此它们无法访问数据库或其他框架服务。
以下是一个简单的单元测试示例:
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
final class UserTest extends TestCase
{
public function testGetId(): void
{
$user = new User(id: 101);
$this->assertSame(101, $user->getId());
}
}
功能测试测试代码的更大一部分,应该扩展 Tests\TestCase
抽象类。这个类是专门设计用来引导您的应用程序,模拟 Web 服务器的行为,但使用测试环境变量。
以下是一个简单的功能测试示例:
namespace Tests\Feature\Controller\UserController;
use Tests\TestCase;
final class ShowActionTest extends TestCase
{
public function testShowPageNotFoundIfUserNotExist(): void
{
$http = $this->fakeHttp();
$response = $http->get('/user/1');
$response->assertNotFound();
}
}
Tests\TestCase
类包含一个功能,允许您为测试用例设置环境变量。如果您需要根据环境变量测试特定行为,这可能会很有用。
use Tests\TestCase;
final class SomeTest extends TestCase
{
public const ENV = [
'DEBUG' => false,
// ...
];
public function testSomeFeature(): void
{
//
}
}
您也可以使用 PHP 属性定义 ENV 变量。这允许更精细地控制测试环境。
use Tests\TestCase;
use Spiral\Testing\Attribute\Env;
final class SomeTest extends TestCase
{
#[Env('DEBUG', false)]
#[Env('APP_ENV', 'production')]
public function testSomeFeature(): void
{
//
}
}
Tests\TestCase
类还包含一个功能,允许您与容器交互。
$container = $this->getContainer();
$this->assertContainerMissed(\Spiral\Queue\QueueConnectionProviderInterface::class);
$this->assertContainerInstantiable(\Spiral\Queue\QueueConnectionProviderInterface::class);
$this->assertContainerBound(\Spiral\Queue\QueueConnectionProviderInterface::class);
检查容器是否绑定到特定的类
$this->assertContainerBound(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class
);
检查容器是否绑定到具有参数的特定类
$this->assertContainerBound(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class,
['foo' => 'bar']
);
您还可以使用回调函数进行额外的检查
$this->assertContainerBound(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class,
['foo' => 'bar'],
function(\Spiral\Queue\QueueManager $manager) {
$this->assertEquals(..., $manager->....)
}
);
$this->assertContainerBoundAsSingleton(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class
);
$this->assertContainerBoundNotAsSingleton(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class
);
在某些情况下,您需要在容器中模拟服务。例如,身份验证服务。
namespace Tests\Feature\Controller\UserController;
use Tests\TestCase;
final class ProfileActionTest extends TestCase
{
public function testShowPageNotFoundIfUserNotExist(): void
{
$auth = $this->mockContainer(\Spiral\Auth\ActorProviderInterface::class);
$auth->shouldReceive('getActor')->with(...)->once()->andReturnNull();
$http = $this->fakeHttp();
$response = $http->get('/user/profile');
$response->assertNotFound();
}
}
让我们假设我们有以下配置:
return [
'basePath' => '/',
'headers' => [
'Content-Type' => 'text/html; charset=UTF-8',
],
'middleware' => [],
];
在某些情况下,您需要为特定的测试定义配置值。您可以使用 PHP 属性来完成。
use Tests\TestCase;
use Spiral\Testing\Attribute\Config;
final class SomeTest extends TestCase
{
#[Config('http.basePath', '/custom')]
#[Config('http.headers.Content-Type', 'text/plain')]
public function testSomeFeature(): void
{
//
}
}
$this->assertConfigMatches('http', [
'basePath' => '/',
'headers' => [
'Content-Type' => 'text/html; charset=UTF-8',
],
'middleware' => [],
]);
$this->assertConfigHasFragments('http', [
'basePath' => '/'
])
/** @var array $config */
$config = $this->getConfig('http');
$this-assertDirectoryAliasDefined('root');
$this->assertDirectoryAliasMatches('runtime', __DIR__.'src/runtime');
$this->cleanupDirectories(
__DIR__.'src/runtime/cache',
__DIR__.'src/runtime/tmp'
);
$this->cleanupDirectoriesByAliases(
'runtime', 'cache', 'logs'
);
您也可以清理运行时目录
$this->cleanUpRuntimeDirectory();
$this->assertCommandRegistered('ping');
您可以在测试用例中运行控制台命令并检查结果。
$output = $this->runCommand('ping', ['site' => 'https://google.com']);
$this->assertStringContainsString('Pong', $output);
您也可以使用以下方法检查输出中的字符串
$this->assertConsoleCommandOutputContainsStrings(
'ping',
['site' => 'https://google.com'],
['Site found', 'Starting ping ...', 'Success!']
);
Tests\TestCase
类还包含一个功能,允许您与启动加载器交互。
$this->assertBootloaderLoaded(\MyPackage\Bootloaders\PackageBootloader::class);
$this->assertBootloaderMissed(\MyPackage\Bootloaders\PackageBootloader::class);
$this->assertDispatcherRegistered(HttpDispatcher::class);
$this->assertDispatcherMissed(HttpDispatcher::class);
您可以通过传递一些绑定作为第二个参数来运行调度器。它将在具有绑定的范围内运行。
$this->serveDispatcher(HttpDispatcher::class, [
\Spiral\Boot\EnvironmentInterface::class => new \Spiral\Boot\Environment([
'foo' => 'bar'
]),
]);
检查调度器是否可以在当前环境中被服务。
$this->assertDispatcherCanBeServed(HttpDispatcher::class);
检查调度器是否不能在当前环境中被服务。
$this->assertDispatcherCannotBeServed(HttpDispatcher::class);
/** @var class-string<\Spiral\Boot\DispatcherInterface>[] $dispatchers */
$dispatchers = $this->getRegisteredDispatchers();
我们可以提供测试脚手架命令的能力。
$this->assertScaffolderCommandSame(
'create:command',
[
'name' => 'TestCommand',
],
expected: <<<'PHP'
<?php
declare(strict_types=1);
namespace Spiral\Testing\Command;
use Spiral\Console\Attribute\Argument;
use Spiral\Console\Attribute\AsCommand;
use Spiral\Console\Attribute\Option;
use Spiral\Console\Attribute\Question;
use Spiral\Console\Command;
#[AsCommand(name: 'test:command')]
final class TestCommand extends Command
{
public function __invoke(): int
{
// Put your command logic here
$this->info('Command logic is not implemented yet');
return self::SUCCESS;
}
}
PHP,
expectedFilename: 'app/src/Command/TestCommand.php',
expectedOutputStrings: [
"Declaration of 'TestCommand' has been successfully written into 'app/src/Command/TestCommand.php",
],
);
$this->assertScaffolderCommandContains(
'create:command',
[
'name' => 'TestCommand',
'--namespace' => 'App\Command',
],
expectedStrings: [
'namespace App\Command;',
],
expectedFilename: 'app/src/TestCommand.php',
);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Not enough arguments (missing: "name").');
$this->assertScaffolderCommandSame(
'create:command',
[],
'',
);
$this->assertScaffolderCommandContains(
'create:command',
[
'name' => 'TestCommand',
'-o' => 'foo',
],
expectedStrings: [
"#[Option(description: 'Argument description')]",
'private bool $foo;'
],
);
当您运行 vendor/bin/phpunit
命令时,它将自动在应用程序的 tests
目录中查找测试文件,并使用 phpunit.xml
文件中指定的配置运行它们。
要运行 Spiral 应用程序中的测试,您可以简单地执行以下命令:
./vendor/bin/phpunit
尽情测试吧!
现在,通过阅读一些文章深入了解基础知识: