spiral/testing

Spiral Framework testing SDK

2.0.0 2022-07-23 13:43 UTC

README

Latest Version on Packagist Total Downloads

Requirements

Make sure that your server is configured with following PHP version and extensions:

  • PHP 7.4+
  • Spiral framework 2.9+

Installation

You can install the package via composer:

composer require spiral/testing

Spiral App testing

TestApp configuration

Tests folders structure:

- 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;
}

TestCase configuration

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
        );
    }
}

Spiral package testing

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 folders structure:

tests
  - app
    - config
      - my-config.php
    - ...
  - src
    - TestCase.php
    - MyFirstTestCase.php

TestCase configuration

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,
            // ...
        ];
    }
}

Usage

Starting callbacks

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();
    }
}

Interaction with Http

$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);

Requests

$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();

Working with uploading files

$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();

Working with storage

// 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');

Interaction with Mailer

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() === 'user@site.com';
    })
}

assertSent

$this->mailer->assertSent(UserRegisteredMail::class, function (MessageInterface $message) {
    return $message->getTo() === 'user@site.com';
})

assertNotSent

$this->mailer->assertNotSent(UserRegisteredMail::class, function (MessageInterface $message) {
    return $message->getTo() === 'user@site.com';
})

assertSentTimes

$this->mailer->assertSentTimes(UserRegisteredMail::class, 1);

assertNothingSent

$this->mailer->assertNothingSent();

Interaction with Queue

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', ...);
}

assertPushed

$this->mailer->assertPushed('mail.job', function (array $data) {
    return $data['handler'] instanceof \Spiral\SendIt\MailJob
        && $data['options']->getQueue() === 'mail'
        && $data['payload']['foo'] === 'bar';
});

assertPushedOnQueue

$this->mailer->assertPushedOnQueue('mail', 'mail.job', function (array $data) {
    return $data['handler'] instanceof \Spiral\SendIt\MailJob
        && $data['payload']['foo'] === 'bar';
});

assertPushedTimes

$this->mailer->assertPushedTimes('mail.job', 2);

assertNotPushed

$this->mailer->assertNotPushed('mail.job', function (array $data) {
    return $data['handler'] instanceof \Spiral\SendIt\MailJob
        && $data['options']->getQueue() === 'mail'
        && $data['payload']['foo'] === 'bar';
});

assertNothingPushed

$this->mailer->assertNothingPushed();

Interactions with container

assertBootloaderLoaded

$this->assertBootloaderLoaded(\MyPackage\Bootloaders\PackageBootloader::class);

assertBootloaderMissed

$this->assertBootloaderMissed(\Spiral\Framework\Bootloaders\Http\HttpBootloader::class);

assertContainerMissed

$this->assertContainerMissed(\Spiral\Queue\QueueConnectionProviderInterface::class);

assertContainerInstantiable

Checking if container can create an object with autowiring

$this->assertContainerInstantiable(\Spiral\Queue\QueueConnectionProviderInterface::class);

assertContainerBound

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->....)
    }
);

assertContainerBoundNotAsSingleton

$this->assertContainerBoundNotAsSingleton(
    \Spiral\Queue\QueueConnectionProviderInterface::class,
    \Spiral\Queue\QueueManager::class
);

assertContainerBoundAsSingleton

$this->assertContainerBoundAsSingleton(
    \Spiral\Queue\QueueConnectionProviderInterface::class,
    \Spiral\Queue\QueueManager::class
);

mockContainer

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);
}

Interaction with dispatcher

assertDispatcherRegistered

$this->assertDispatcherRegistered(HttpDispatcher::class);

assertDispatcherMissed

$this->assertDispatcherMissed(HttpDispatcher::class);

serveDispatcher

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'
    ]),

]);

getRegisteredDispatchers

/** @var class-string[] $dispatchers */
$dispatchers = $this->getRegisteredDispatchers();

Interaction with Console

assertConsoleCommandOutputContainsStrings

$this->assertConsoleCommandOutputContainsStrings(
    'ping',
    ['site' => 'https://google.com'],
    ['Site found', 'Starting ping ...', 'Success!']
);

runCommand

$output = $this->runCommand('ping', ['site' => 'https://google.com']);

foreach (['Site found', 'Starting ping ...', 'Success!'] as $string) {
    $this->assertStringContaisString($string, $output);
}

Interaction with Config

assertConfigMatches

$this->assertConfigMatches('http', [
    'basePath'   => '/',
    'headers'    => [
        'Content-Type' => 'text/html; charset=UTF-8',
    ],
    'middleware' => [],
])

getConfig

/** @var array $config */
$config = $this->getConfig('http');

Interactions with file system

assertDirectoryAliasDefined

$this->assertDirectoryAliasDefined('runtime');

assertDirectoryAliasMatches

$this->assertDirectoryAliasMatches('runtime', __DIR__.'src/runtime');

cleanupDirectories

$this->cleanupDirectories(
    __DIR__.'src/runtime/cache',
    __DIR__.'src/runtime/tmp'
);

cleanupDirectoriesByAliases

$this->cleanupDirectoriesByAliases(
    'runtime', 'app', '...'
);

cleanUpRuntimeDirectory

$this->cleanUpRuntimeDirectory();

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.