Skip to content

Latest commit

 

History

History
827 lines (601 loc) · 17.6 KB

README.md

File metadata and controls

827 lines (601 loc) · 17.6 KB

Spiral Framework testing SDK

Latest Version on Packagist Total Downloads

Requirements

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

  • PHP 8.1+
  • Spiral framework 3.0+

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 to test 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

Application running

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: [
            // ...
        ]);
    }
}

Environment variables

You have two options to pass ENV variables to into your application instance:

  1. By using ENV const.
class KernelTest extends BaseTest
{
    public const ENV = [
        'FOO' => 'BAR'
    ];
    
    public function testSomeFeature(): void
    {
        // 
    }
}
  1. By running application by yourself
final class SomeTest extends BaseTest
{
    public const MAKE_APP_ON_STARTUP = false;
    
    public function testSomeFeature(): void
    {
        $this->initApp(env: [
            // ...
        ]);
    }
}

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

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

Request response

/** @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();

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() === '[email protected]';
    })
}

assertSent

$this->mailer->assertSent(UserRegisteredMail::class, function (MessageInterface $message) {
    return $message->getTo() === '[email protected]';
})

assertNotSent

$this->mailer->assertNotSent(UserRegisteredMail::class, function (MessageInterface $message) {
    return $message->getTo() === '[email protected]';
})

assertSentTimes

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

assertNothingSent

$this->mailer->assertNothingSent();

Interaction with Events

protected function setUp(): void
{
    parent::setUp();
    $this->eventDispatcher = $this->fakeEventDispatcher();
}

assertListening

Assert if an event has a listener attached to it.

$this->eventDispatcher->assertListening(SomeEvent::class, SomeListener::class);

assertDispatched

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

assertDispatchedTimes

Assert if an event was dispatched a number of times.

$this->eventDispatcher->assertDispatchedTimes(SomeEvent::class, 5);

assertNotDispatched

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

assertNothingDispatched

Assert that no events were dispatched.

$this->eventDispatcher->assertNothingDispatched();

dispatched

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

hasDispatched

$this->eventDispatcher->hasDispatched(SomeEvent::class);

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

]);

assertDispatcherCanBeServed

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

assertDispatcherCannotBeServed

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

getRegisteredDispatchers

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

Interaction with Console

assertConsoleCommandOutputContainsStrings

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

assertCommandRegistered

$this->assertCommandRegistered('ping');

runCommand

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

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

Interaction with Views

assertViewSame

$this->assertViewSame('foo:bar', [
    'foo' => 'bar',
], '<html>...</html>')

assertViewContains

$this->assertViewContains('foo:bar', [
    'foo' => 'bar',
], ['<div>...</div>', '<a href="...">...</a>'])

assertViewContains

$this->assertViewNotContains('foo:bar', [
    'foo' => 'bar',
], ['<div class="hidden">...</div>'])

assertViewContains with specific locale

$this->withLocale('fr')->assertViewSame('foo:bar', [
    'foo' => 'bar',
], '<div>...</div>')

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.