Make sure that your server is configured with following PHP version and extensions:
- PHP 7.4+
- Spiral framework 2.9+
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 testing 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,
// ...
];
}
}
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() !!!
$this->beforeStarting(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();
$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->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'
]),
]);
/** @var class-string[] $dispatchers */
$dispatchers = $this->getRegisteredDispatchers();
$this->assertConsoleCommandOutputContainsStrings(
'ping',
['site' => 'https://google.com'],
['Site found', 'Starting ping ...', 'Success!']
);
$output = $this->runCommand('ping', ['site' => 'https://google.com']);
foreach (['Site found', 'Starting ping ...', 'Success!'] as $string) {
$this->assertStringContaisString($string, $output);
}
$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.