β½ Octane Testbench
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
- is progressive: existing tests keep working, making Octane adoption easier for existing Laravel apps
- stubs out workers and clients: tests don't need a Swoole or RoadRunner server to run
- preserves the application state after a request, so assertions can be performed after the response
- 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 [email protected] instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.