cerbero/octane-testbench

Set of utilities to test Laravel applications powered by Octane.

Fund package maintenance!
cerbero90

1.0.1 2022-05-10 15:20 UTC

This package is auto-updated.

Last update: 2024-11-11 20:01:01 UTC


README

Author PHP Version Laravel Version Octane Compatibility Build Status Coverage Status Quality Score Latest Version Software License PSR-12 Total Downloads

Set of utilities to test Laravel applications powered by Octane.

Install

Via Composer:

composer require --dev cerbero/octane-testbench

In tests/TestCase.php, use the TestsOctaneApplication trait:

use Cerbero\OctaneTestbench\TestsOctaneApplication;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use TestsOctaneApplication;
}

Now all tests extending this class, even previously created tests, can run on Octane.

Usage

In a nutshell, Octane Testbench

  1. is progressive: existing tests keep working, making Octane adoption easier for existing Laravel apps
  2. stubs out workers and clients: tests don't need a Swoole or RoadRunner server to run
  3. preserves the application state after a request, so assertions can be performed after the response
  4. offers fluent assertions tailored to Octane:
public function test_octane_application()
{
    $this
        ->assertOctaneCacheMissing('foo')
        ->assertOctaneTableMissing('example', 'row')
        ->assertOctaneTableCount('example', 0)
        ->expectsConcurrencyResults([1, 2, 3])
        ->get('octane/route')
        ->assertOk()
        ->assertOctaneCacheHas('foo', 'bar')
        ->assertOctaneTableHas('example', 'row.votes', 123)
        ->assertOctaneTableCount('example', 1);
}

Requests and responses

HTTP requests are performed with the same methods we would normally call to test any Laravel application, except they will work for both standard and Octane routes:

Route::get('web-route', fn () => 123);

Octane::route('POST', '/octane-route', fn () => new Response('foo'));


public function test_web_route()
{
    $this->get('web-route')->assertOk()->assertSee('123');
}

public function test_octane_route()
{
    $this->post('octane-route')->assertOk()->assertSee('foo');
}

Responses are wrapped in a ResponseTestCase instance that lets us call response assertions, any assertion of the Laravel testing suite and the following exception assertions:

$this
    ->get('failing-route')
    ->assertException(Exception::class) // assert exception instance
    ->assertException(new Exception('message')) // assert exception instance and message
    ->assertExceptionMessage('message'); // assert exception message

Furthermore, responses and exceptions can be debugged by calling the dd() and dump() methods:

$this
    ->get('failing-route')
    ->dump() // dump the whole response/exception
    ->dump('original') // dump only a specific property
    ->dd() // dump-and-die the whole response/exception
    ->dd('headers'); // dump-and-die only a specific property

Concurrency

Concurrency works fine during tests. However, PHP 8 forbids the serialization of reflections (hence mocks) and concurrent tasks are serialized before being dispatched. If tasks involve mocks, we can fake the concurrency:

// code to test:
Octane::concurrently([
    fn () => $mockedService->run(),
    fn () => 123,
]);

// test:
$this
    ->mocks(Service::class, ['run' => 'foo'])
    ->expectsConcurrency()
    ->get('route');

In the test above we are running tasks sequentially without serialization, allowing mocked methods to be executed (we will see more about mocks later).

If we need more control over how concurrent tasks run, we can pass a closure to expectsConcurrency(). For example, the test below runs only the first task:

$this
    ->expectsConcurrency(fn (array $tasks) => [ $tasks[0]() ])
    ->get('route');

To manipulate the results of concurrent tasks, we can use expectsConcurrencyResults():

$this
    ->expectsConcurrencyResults([$firstTaskResult, $secondTaskResult])
    ->get('route');

Finally we can make concurrent tasks fail to test our code when something wrong happens:

$this
    ->expectsConcurrencyException() // tasks fail due to a generic exception
    ->get('route');

$this
    ->expectsConcurrencyException(new Exception('message')) // tasks fail due to a specific exception
    ->get('route');

$this
    ->expectsConcurrencyTimeout() // tasks fail due to a timeout
    ->get('route');

Cache

Octane Testbench provides the following assertions to test the Octane cache:

$this
    ->assertOctaneCacheMissing($key) // assert the key is not set
    ->get('route')
    ->assertOctaneCacheHas($key) // assert the key is set
    ->assertOctaneCacheHas($key, $value); // assert the key has the given value

Tables

Octane tables can be tested with the following assertions:

$this
    ->assertOctaneTableMissing($table, $row) // assert the row is not present in the table
    ->assertOctaneTableCount($table, 0) // assert the number of rows in the table
    ->get('route')
    ->assertOctaneTableHas($table, $row) // assert the row is present in the table
    ->assertOctaneTableHas($table, 'row.column' $value) // assert the column in the row has the given value
    ->assertOctaneTableCount($table, 1);

Events

By default listeners for the Octane RequestReceived event are disabled to perform assertions on the application state. However we can register listeners for any Octane event if need be:

$this
    ->listensTo(RequestReceived::class, $listener1, $listener2) // register 2 listeners for RequestReceived
    ->get('route');

$this
    ->stopsListeningTo(TaskReceived::class, $listener1, $listener2) // unregister 2 listeners for TaskReceived
    ->get('route');

Container

Octane Testbench also introduces the following helpers to bind and mock services at the same time while preserving a fluent syntax:

$this
    ->mocks(Service::class, ['expectedMethod' => $expectedValue]) // mock with simple expectations
    ->mocks(Service::class, fn ($mock) => $mock->shouldReceive('method')->twice()) // mock with advanced expectations
    ->partiallyMocks(Service::class, ['expectedMethod' => $expectedValue]) // same as above but partial
    ->partiallyMocks(Service::class, fn ($mock) => $mock->shouldReceive('method')->twice())
    ->get('route');

Change log

Please see CHANGELOG for more information on what has changed recently.

Testing

composer test

Contributing

Please see CONTRIBUTING and CODE_OF_CONDUCT for details.

Security

If you discover any security related issues, please email andrea.marco.sartori@gmail.com instead of using the issue tracker.

Credits

License

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