Make sure that your server is configured with following PHP version and extensions:
- PHP 8.1+
- Spiral framework 3.0+
You can install the package via composer:
composer require spiral/testing
- tests
- TestCase.php
- Unit
- MyFirstTestCase.php
- ...
- Feature
- Controllers
- HomeControllerTestCase.php
...
- TestApp.php
Create test App class and implement Spiral\Testing\TestableKernelInterface
namespace Tests\App;
class TestApp extends \App\App implements \Spiral\Testing\TestableKernelInterface
{
use \Spiral\Testing\Traits\TestableKernel;
}
Extend your TestCase class from Spiral\Testing\TestCase
and implements a couple of required methods:
namespace Tests;
abstract class TestCase extends \Spiral\Testing\TestCase
{
public function createAppInstance(): TestableKernelInterface
{
return \Spiral\Tests\App\TestApp::create(
$this->defineDirectories($this->rootDirectory()),
false
);
}
}
There are some difference between App and package testing. One of them - tou don't have application and bootloaders.
TestCase from the package has custom TestApp implementation that will help you to test your packages without creating extra classes.
The following example will show you how it is easy-peasy.
tests
- app
- config
- my-config.php
- ...
- src
- TestCase.php
- MyFirstTestCase.php
namespace MyPackage\Tests;
abstract class TestCase extends \Spiral\Testing\TestCase
{
public function rootDirectory(): string
{
return __DIR__.'/../';
}
public function defineBootloaders(): array
{
return [
\MyPackage\Bootloaders\PackageBootloader::class,
// ...
];
}
}
Application will be run automatically via setUp
method in Spiral\Testing\TestCase
. If you need to run application by
your self, you may disable automatic running.
final class SomeTest extends BaseTest
{
public const MAKE_APP_ON_STARTUP = false;
public function testSomeFeature(): void
{
$this->initApp(env: [
// ...
]);
}
}
You have two options to pass ENV variables to into your application instance:
- By using
ENV
const.
class KernelTest extends BaseTest
{
public const ENV = [
'FOO' => 'BAR'
];
public function testSomeFeature(): void
{
//
}
}
- By running application by yourself
final class SomeTest extends BaseTest
{
public const MAKE_APP_ON_STARTUP = false;
public function testSomeFeature(): void
{
$this->initApp(env: [
// ...
]);
}
}
If you need to rebind some bound containers, you can do it via starting callbacks. You can create as more callbacks as you want.
Make sure that you create callbacks before application run.
abstract class TestCase extends \Spiral\Testing\TestCase
{
protected function setUp(): void
{
// !!! Before parent::setUp() !!!
// Before application init
$this->beforeInit(static function(\Spiral\Core\Container $container) {
$container->bind(\Spiral\Queue\QueueInterface::class, // ...);
});
// Before application booting
$this->beforeBooting(static function(\Spiral\Core\Container $container) {
$container->bind(\Spiral\Queue\QueueInterface::class, // ...);
});
parent::setUp();
}
}
$response = $this->fakeHttp()
->withHeaders(['Accept' => 'application/json'])
->withHeader('CONTENT_TYPE', 'application/json')
->withActor(new UserActor())
->withServerVariables(['SERVER_ADDR' => '127.0.0.1'])
->withAuthorizationToken('token-hash', 'Bearer') // Header => Authorization: Bearer token-hash
->withCookie('csrf', '...')
->withSession([
'cart' => [
'items' => [...]
]
])
->withEnvironment([
'QUEUE_CONNECTION' => 'sync'
])
->withoutMiddleware(MyMiddleware::class)
->get('/post/1')
$response->assertStatus(200);
$http = $this->fakeHttp();
$http->withHeaders(['Accept' => 'application/json']);
$http->get(uri: '/', query: ['foo' => 'bar'])->assertOk();
$http->getJson(uri: '/')->assertOk();
$http->post(uri: '/', data: ['foo' => 'bar'], headers: ['Content-type' => '...'])->assertOk();
$http->postJson(uri: '/')->assertOk();
$http->put(uri: '/', cookies: ['token' => '...'])->assertOk();
$http->putJson(uri: '/')->assertOk();
$http->delete(uri: '/')->assertOk();
$http->deleteJson(uri: '/')->assertOk();
/** @var \Spiral\Testing\Http\FakeHttp $http */
$response = $http->get(uri: '/', query: ['foo' => 'bar']);
// Check if header presents in response
$response->assertHasHeader('Content-type');
// Check if header missed in response
$response->assertHeaderMissing('Content-type');
// Get status code
$code = $response->getStatusCode();
// Check status code
$response->assertStatus(200);
$response->assertOk(); // code: 200
$response->assertCreated(); // code: 201
$response->assertAccepted(); // code:
$response->assertNoContent(); // code: 204
$response->assertNoContent(status: 204); // code: 204
$response->assertNotFound(); // code: 404
$response->assertForbidden(); // code: 403
$response->assertUnauthorized(); // code: 401
$response->assertUnprocessable(); // code: 422
// Check body
$response->assertBodySame('OK');
$response->assertBodyNotSame('OK');
$response->assertBodyContains('Hello world');
// Get body content
$body = (string) $response;
// Check cookie
$response->assertCookieExists('foo');
$response->assertCookieMissed('foo');
$response->assertCookieSame(key: 'foo', value: 'bar');
$cookies = $response->getCookies();
// Check if response is redirect to another page
$this->assertTrue($response->isRedirect());
// Get original response
$response = $response->getOriginalResponse();
$http = $this->fakeHttp();
// Create a file with size - 100kb
$file = $http->getFileFactory()->createFile('foo.txt', 100);
// Create a file with specific content
$file = $http->getFileFactory()->createFileWithContent('foo.txt', 'Hello world');
// Create a fake image 640x480
$image = $http->getFileFactory()->createImage('fake.jpg', 640, 480);
$http->post(uri: '/', files: ['avatar' => $image, 'documents' => [$file]])->assertOk();
// Will replace all buckets into with local adapters
$storage = $this->fakeStorage();
// Do something with storage
// $image = new UploadedFile(...);
// $storage->bucket('uploads')->write(
// $image->getClientFilename(),
// $image->getStream()
// );
$uploads = $storage->bucket('uploads');
$uploads->assertExists('image.jpg');
$uploads->assertCreated('image.jpg');
$public = $storage->bucket('public');
$public->assertNotExist('image.jpg');
$public->assertNotCreated('image.jpg');
// $public->delete('file.txt');
$public->assertDeleted('file.txt');
$uploads->assertNotDeleted('file.txt');
$public->assertNotExist('file.txt');
// $public->move('file.txt', 'folder/file.txt');
$public->assertMoved('file.txt', 'folder/file.txt');
$uploads->assertNotMoved('file.txt', 'folder/file.txt');
// $public->copy('file.txt', 'folder/file.txt');
$public->assertCopied('file.txt', 'folder/file.txt');
$uploads->assertNotCopied('file.txt', 'folder/file.txt');
// $public->setVisibility('file.txt', 'public');
$public->assertVisibilityChanged('file.txt');
$uploads->assertVisibilityNotChanged('file.txt');
protected function setUp(): void
{
parent::setUp();
$this->mailer = $this->fakeMailer();
}
protected function testRegisterUser(): void
{
// run some code
$this->mailer->assertSent(UserRegisteredMail::class, function (MessageInterface $message) {
return $message->getTo() === '[email protected]';
})
}
$this->mailer->assertSent(UserRegisteredMail::class, function (MessageInterface $message) {
return $message->getTo() === '[email protected]';
})
$this->mailer->assertNotSent(UserRegisteredMail::class, function (MessageInterface $message) {
return $message->getTo() === '[email protected]';
})
$this->mailer->assertSentTimes(UserRegisteredMail::class, 1);
$this->mailer->assertNothingSent();
protected function setUp(): void
{
parent::setUp();
$this->eventDispatcher = $this->fakeEventDispatcher();
}
Assert if an event has a listener attached to it.
$this->eventDispatcher->assertListening(SomeEvent::class, SomeListener::class);
Assert if an event was dispatched based on a truth-test callback.
// Assert if an event dispatched one or more times
$this->eventDispatcher->assertDispatched(SomeEvent::class);
// Assert if an event dispatched one or more times based on a truth-test callback.
$this->eventDispatcher->assertDispatched(SomeEvent::class, static function(SomeEvent $event): bool {
return $event->someParam === 100;
});
Assert if an event was dispatched a number of times.
$this->eventDispatcher->assertDispatchedTimes(SomeEvent::class, 5);
Determine if an event was dispatched based on a truth-test callback.
$this->eventDispatcher->assertNotDispatched(SomeEvent::class);
$this->eventDispatcher->assertNotDispatched(SomeEvent::class, static function(SomeEvent $event): bool {
return $event->someParam === 100;
});
Assert that no events were dispatched.
$this->eventDispatcher->assertNothingDispatched();
Get all the events matching a truth-test callback.
$this->eventDispatcher->dispatched(SomeEvent::class);
// or
$this->eventDispatcher->dispatched(SomeEvent::class, static function(SomeEvent $event): bool {
return $event->someParam === 100;
});
$this->eventDispatcher->hasDispatched(SomeEvent::class);
protected function setUp(): void
{
parent::setUp();
$this->connection = $this->fakeQueue();
$this->queue = $this->connection->getConnection();
}
protected function testRegisterUser(): void
{
// run some code
$this->queue->assertPushed('mail.job', function (array $data) {
return $data['handler'] instanceof \Spiral\SendIt\MailJob
&& $data['options']->getQueue() === 'mail'
&& $data['payload']['foo'] === 'bar';
});
$this->connection->getConnection('redis')->assertPushed('another.job', ...);
}
$this->mailer->assertPushed('mail.job', function (array $data) {
return $data['handler'] instanceof \Spiral\SendIt\MailJob
&& $data['options']->getQueue() === 'mail'
&& $data['payload']['foo'] === 'bar';
});
$this->mailer->assertPushedOnQueue('mail', 'mail.job', function (array $data) {
return $data['handler'] instanceof \Spiral\SendIt\MailJob
&& $data['payload']['foo'] === 'bar';
});
$this->mailer->assertPushedTimes('mail.job', 2);
$this->mailer->assertNotPushed('mail.job', function (array $data) {
return $data['handler'] instanceof \Spiral\SendIt\MailJob
&& $data['options']->getQueue() === 'mail'
&& $data['payload']['foo'] === 'bar';
});
$this->mailer->assertNothingPushed();
$this->assertBootloaderLoaded(\MyPackage\Bootloaders\PackageBootloader::class);
$this->assertBootloaderMissed(\Spiral\Framework\Bootloaders\Http\HttpBootloader::class);
$this->assertContainerMissed(\Spiral\Queue\QueueConnectionProviderInterface::class);
Checking if container can create an object with autowiring
$this->assertContainerInstantiable(\Spiral\Queue\QueueConnectionProviderInterface::class);
Checking if container has alias and bound with the same interface
$this->assertContainerBound(\Spiral\Queue\QueueConnectionProviderInterface::class);
Checking if container has alias with specific class
$this->assertContainerBound(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class
);
// With additional parameters
$this->assertContainerBound(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class,
[
'foo' => 'bar'
]
);
// With callback
$this->assertContainerBound(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class,
[
'foo' => 'bar'
],
function(\Spiral\Queue\QueueManager $manager) {
$this->assertEquals(..., $manager->....)
}
);
$this->assertContainerBoundNotAsSingleton(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class
);
$this->assertContainerBoundAsSingleton(
\Spiral\Queue\QueueConnectionProviderInterface::class,
\Spiral\Queue\QueueManager::class
);
The method will bind alias with mock in the application container.
function testQueue(): void
{
$manager = $this->mockContainer(\Spiral\Queue\QueueConnectionProviderInterface::class);
$manager->shouldReceive('getConnection')->once()->with('foo')->andReturn(
\Mockery::mock(\Spiral\Queue\QueueInterface::class)
);
$queue = $this->getContainer()->get(\Spiral\Queue\QueueInterface::class);
}
$this->assertDispatcherRegistered(HttpDispatcher::class);
$this->assertDispatcherMissed(HttpDispatcher::class);
Check if dispatcher registered in the application and run method serve inside scope with passed bindings.
$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[] $dispatchers */
$dispatchers = $this->getRegisteredDispatchers();
$this->assertConsoleCommandOutputContainsStrings(
'ping',
['site' => 'https://google.com'],
['Site found', 'Starting ping ...', 'Success!']
);
$this->assertCommandRegistered('ping');
$output = $this->runCommand('ping', ['site' => 'https://google.com']);
foreach (['Site found', 'Starting ping ...', 'Success!'] as $string) {
$this->assertStringContaisString($string, $output);
}
$this->assertViewSame('foo:bar', [
'foo' => 'bar',
], '<html>...</html>')
$this->assertViewContains('foo:bar', [
'foo' => 'bar',
], ['<div>...</div>', '<a href="...">...</a>'])
$this->assertViewNotContains('foo:bar', [
'foo' => 'bar',
], ['<div class="hidden">...</div>'])
$this->withLocale('fr')->assertViewSame('foo:bar', [
'foo' => 'bar',
], '<div>...</div>')
$this->assertConfigMatches('http', [
'basePath' => '/',
'headers' => [
'Content-Type' => 'text/html; charset=UTF-8',
],
'middleware' => [],
])
/** @var array $config */
$config = $this->getConfig('http');
$this->assertDirectoryAliasDefined('runtime');
$this->assertDirectoryAliasMatches('runtime', __DIR__.'src/runtime');
$this->cleanupDirectories(
__DIR__.'src/runtime/cache',
__DIR__.'src/runtime/tmp'
);
$this->cleanupDirectoriesByAliases(
'runtime', 'app', '...'
);
$this->cleanUpRuntimeDirectory();
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.