├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Concerns ├── CreatesApplication.php ├── InteractsWithCache.php ├── InteractsWithContainer.php ├── InteractsWithEvents.php ├── InteractsWithTables.php ├── MakesHttpRequests.php └── ProvidesConcurrencySupport.php ├── ResponseTestCase.php ├── Stubs ├── ApplicationFactoryStub.php ├── ClientStub.php └── WorkerStub.php └── TestsOctaneApplication.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `octane-testbench` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | ## 1.0.1 - 2022-05-11 8 | 9 | ### Fixed 10 | - Method signature compatibility in ResponseTestCase 11 | 12 | 13 | ## 1.0.0 - 2021-06-10 14 | 15 | ### Added 16 | - Documentation 17 | - Public release 18 | 19 | 20 | ## 0.1.0 - 2021-06-09 21 | 22 | ### Added 23 | - First implementation 24 | 25 | 26 | ## NEXT - YYYY-MM-DD 27 | 28 | ### Added 29 | - Nothing 30 | 31 | ### Deprecated 32 | - Nothing 33 | 34 | ### Fixed 35 | - Nothing 36 | 37 | ### Removed 38 | - Nothing 39 | 40 | ### Security 41 | - Nothing 42 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `andrea.marco.sartori@gmail.com`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/cerbero90/octane-testbench). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-12 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-12-extended-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Andrea Marco Sartori 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⛽ Octane Testbench 2 | 3 | [![Author][ico-author]][link-author] 4 | [![PHP Version][ico-php]][link-php] 5 | [![Laravel Version][ico-laravel]][link-laravel] 6 | [![Octane Compatibility][ico-octane]][link-octane] 7 | [![Build Status][ico-actions]][link-actions] 8 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer] 9 | [![Quality Score][ico-code-quality]][link-code-quality] 10 | [![Latest Version][ico-version]][link-packagist] 11 | [![Software License][ico-license]](LICENSE.md) 12 | [![PSR-12][ico-psr12]][link-psr12] 13 | [![Total Downloads][ico-downloads]][link-downloads] 14 | 15 | Set of utilities to test Laravel applications powered by Octane. 16 | 17 | 18 | ## Install 19 | 20 | Via Composer: 21 | 22 | ``` bash 23 | composer require --dev cerbero/octane-testbench 24 | ``` 25 | 26 | In `tests/TestCase.php`, use the `TestsOctaneApplication` trait: 27 | 28 | ```php 29 | use Cerbero\OctaneTestbench\TestsOctaneApplication; 30 | use Illuminate\Foundation\Testing\TestCase as BaseTestCase; 31 | 32 | abstract class TestCase extends BaseTestCase 33 | { 34 | use TestsOctaneApplication; 35 | } 36 | ``` 37 | 38 | Now all tests extending this class, even previously created tests, can run on Octane. 39 | 40 | ## Usage 41 | 42 | * [Requests and responses](#requests-and-responses) 43 | * [Concurrency](#concurrency) 44 | * [Cache](#cache) 45 | * [Tables](#tables) 46 | * [Events](#events) 47 | * [Container](#container) 48 | 49 | In a nutshell, Octane Testbench 50 | 51 | 1. is progressive: existing tests keep working, making Octane adoption easier for existing Laravel apps 52 | 1. stubs out workers and clients: tests don't need a Swoole or RoadRunner server to run 53 | 1. preserves the application state after a request, so assertions can be performed after the response 54 | 1. offers fluent assertions tailored to Octane: 55 | 56 | ```php 57 | public function test_octane_application() 58 | { 59 | $this 60 | ->assertOctaneCacheMissing('foo') 61 | ->assertOctaneTableMissing('example', 'row') 62 | ->assertOctaneTableCount('example', 0) 63 | ->expectsConcurrencyResults([1, 2, 3]) 64 | ->get('octane/route') 65 | ->assertOk() 66 | ->assertOctaneCacheHas('foo', 'bar') 67 | ->assertOctaneTableHas('example', 'row.votes', 123) 68 | ->assertOctaneTableCount('example', 1); 69 | } 70 | ``` 71 | 72 | 73 | ### Requests and responses 74 | 75 | HTTP requests are performed with the [same methods](https://laravel.com/docs/http-tests#making-requests) we would normally call to test any Laravel application, except they will work for both standard and Octane routes: 76 | 77 | ```php 78 | Route::get('web-route', fn () => 123); 79 | 80 | Octane::route('POST', '/octane-route', fn () => new Response('foo')); 81 | 82 | 83 | public function test_web_route() 84 | { 85 | $this->get('web-route')->assertOk()->assertSee('123'); 86 | } 87 | 88 | public function test_octane_route() 89 | { 90 | $this->post('octane-route')->assertOk()->assertSee('foo'); 91 | } 92 | ``` 93 | 94 | Responses are wrapped in a `ResponseTestCase` instance that lets us call [response assertions](https://laravel.com/docs/http-tests#available-assertions), any assertion of the [Laravel testing suite](https://laravel.com/docs/testing) and the following exception assertions: 95 | 96 | ```php 97 | $this 98 | ->get('failing-route') 99 | ->assertException(Exception::class) // assert exception instance 100 | ->assertException(new Exception('message')) // assert exception instance and message 101 | ->assertExceptionMessage('message'); // assert exception message 102 | ``` 103 | 104 | Furthermore, responses and exceptions can be debugged by calling the `dd()` and `dump()` methods: 105 | 106 | ```php 107 | $this 108 | ->get('failing-route') 109 | ->dump() // dump the whole response/exception 110 | ->dump('original') // dump only a specific property 111 | ->dd() // dump-and-die the whole response/exception 112 | ->dd('headers'); // dump-and-die only a specific property 113 | ``` 114 | 115 | 116 | ### Concurrency 117 | 118 | [Concurrency](https://laravel.com/docs/octane#concurrent-tasks) 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: 119 | 120 | ```php 121 | // code to test: 122 | Octane::concurrently([ 123 | fn () => $mockedService->run(), 124 | fn () => 123, 125 | ]); 126 | 127 | // test: 128 | $this 129 | ->mocks(Service::class, ['run' => 'foo']) 130 | ->expectsConcurrency() 131 | ->get('route'); 132 | ``` 133 | In the test above we are running tasks sequentially without serialization, allowing mocked methods to be executed (we will see more about [mocks](#container) later). 134 | 135 | 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: 136 | 137 | ```php 138 | $this 139 | ->expectsConcurrency(fn (array $tasks) => [ $tasks[0]() ]) 140 | ->get('route'); 141 | ``` 142 | 143 | To manipulate the results of concurrent tasks, we can use `expectsConcurrencyResults()`: 144 | 145 | ```php 146 | $this 147 | ->expectsConcurrencyResults([$firstTaskResult, $secondTaskResult]) 148 | ->get('route'); 149 | ``` 150 | 151 | Finally we can make concurrent tasks fail to test our code when something wrong happens: 152 | 153 | ```php 154 | $this 155 | ->expectsConcurrencyException() // tasks fail due to a generic exception 156 | ->get('route'); 157 | 158 | $this 159 | ->expectsConcurrencyException(new Exception('message')) // tasks fail due to a specific exception 160 | ->get('route'); 161 | 162 | $this 163 | ->expectsConcurrencyTimeout() // tasks fail due to a timeout 164 | ->get('route'); 165 | ``` 166 | 167 | 168 | ### Cache 169 | 170 | Octane Testbench provides the following assertions to test the [Octane cache](https://laravel.com/docs/octane#the-octane-cache): 171 | 172 | ```php 173 | $this 174 | ->assertOctaneCacheMissing($key) // assert the key is not set 175 | ->get('route') 176 | ->assertOctaneCacheHas($key) // assert the key is set 177 | ->assertOctaneCacheHas($key, $value); // assert the key has the given value 178 | ``` 179 | 180 | 181 | ### Tables 182 | 183 | [Octane tables](https://laravel.com/docs/octane#tables) can be tested with the following assertions: 184 | 185 | ```php 186 | $this 187 | ->assertOctaneTableMissing($table, $row) // assert the row is not present in the table 188 | ->assertOctaneTableCount($table, 0) // assert the number of rows in the table 189 | ->get('route') 190 | ->assertOctaneTableHas($table, $row) // assert the row is present in the table 191 | ->assertOctaneTableHas($table, 'row.column' $value) // assert the column in the row has the given value 192 | ->assertOctaneTableCount($table, 1); 193 | ``` 194 | 195 | 196 | ### Events 197 | 198 | 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: 199 | 200 | ```php 201 | $this 202 | ->listensTo(RequestReceived::class, $listener1, $listener2) // register 2 listeners for RequestReceived 203 | ->get('route'); 204 | 205 | $this 206 | ->stopsListeningTo(TaskReceived::class, $listener1, $listener2) // unregister 2 listeners for TaskReceived 207 | ->get('route'); 208 | ``` 209 | 210 | 211 | ### Container 212 | 213 | Octane Testbench also introduces the following helpers to bind and [mock services](https://docs.mockery.io/en/latest/reference/index.html) at the same time while preserving a fluent syntax: 214 | 215 | ```php 216 | $this 217 | ->mocks(Service::class, ['expectedMethod' => $expectedValue]) // mock with simple expectations 218 | ->mocks(Service::class, fn ($mock) => $mock->shouldReceive('method')->twice()) // mock with advanced expectations 219 | ->partiallyMocks(Service::class, ['expectedMethod' => $expectedValue]) // same as above but partial 220 | ->partiallyMocks(Service::class, fn ($mock) => $mock->shouldReceive('method')->twice()) 221 | ->get('route'); 222 | ``` 223 | 224 | ## Change log 225 | 226 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 227 | 228 | ## Testing 229 | 230 | ``` bash 231 | composer test 232 | ``` 233 | 234 | ## Contributing 235 | 236 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. 237 | 238 | ## Security 239 | 240 | If you discover any security related issues, please email andrea.marco.sartori@gmail.com instead of using the issue tracker. 241 | 242 | ## Credits 243 | 244 | - [Andrea Marco Sartori][link-author] 245 | - [All Contributors][link-contributors] 246 | 247 | ## License 248 | 249 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 250 | 251 | [ico-author]: https://img.shields.io/static/v1?label=author&message=cerbero90&color=50ABF1&logo=twitter&style=flat-square 252 | [ico-php]: https://img.shields.io/packagist/php-v/cerbero/octane-testbench?color=%234F5B93&logo=php&style=flat-square 253 | [ico-laravel]: https://img.shields.io/static/v1?label=laravel&message=%E2%89%A58.0&color=ff2d20&logo=laravel&style=flat-square 254 | [ico-octane]: https://img.shields.io/static/v1?label=octane&message=compatible&color=ff2d20&logo=laravel&style=flat-square 255 | [ico-version]: https://img.shields.io/packagist/v/cerbero/octane-testbench.svg?label=version&style=flat-square 256 | [ico-actions]: https://img.shields.io/github/workflow/status/cerbero90/octane-testbench/build?style=flat-square&logo=github 257 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 258 | [ico-psr12]: https://img.shields.io/static/v1?label=compliance&message=PSR-12&color=blue&style=flat-square 259 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/cerbero90/octane-testbench.svg?style=flat-square&logo=scrutinizer 260 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/cerbero90/octane-testbench.svg?style=flat-square&logo=scrutinizer 261 | [ico-downloads]: https://img.shields.io/packagist/dt/cerbero/octane-testbench.svg?style=flat-square 262 | 263 | [link-author]: https://twitter.com/cerbero90 264 | [link-php]: https://www.php.net 265 | [link-laravel]: https://laravel.com 266 | [link-octane]: https://github.com/laravel/octane 267 | [link-packagist]: https://packagist.org/packages/cerbero/octane-testbench 268 | [link-actions]: https://github.com/cerbero90/octane-testbench/actions?query=workflow%3Abuild 269 | [link-psr12]: https://www.php-fig.org/psr/psr-12/ 270 | [link-scrutinizer]: https://scrutinizer-ci.com/g/cerbero90/octane-testbench/code-structure 271 | [link-code-quality]: https://scrutinizer-ci.com/g/cerbero90/octane-testbench 272 | [link-downloads]: https://packagist.org/packages/cerbero/octane-testbench 273 | [link-contributors]: ../../contributors 274 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cerbero/octane-testbench", 3 | "type": "library", 4 | "description": "Set of utilities to test Laravel applications powered by Octane.", 5 | "keywords": [ 6 | "laravel", 7 | "octane", 8 | "laravel-octane", 9 | "test", 10 | "testing" 11 | ], 12 | "homepage": "https://github.com/cerbero90/octane-testbench", 13 | "license": "MIT", 14 | "authors": [{ 15 | "name": "Andrea Marco Sartori", 16 | "email": "andrea.marco.sartori@gmail.com", 17 | "homepage": "https://github.com/cerbero90", 18 | "role": "Developer" 19 | }], 20 | "require": { 21 | "php": "^8.0", 22 | "laravel/octane": "^1.0", 23 | "mockery/mockery": "^1.0", 24 | "phpunit/phpunit": ">=9.3" 25 | }, 26 | "require-dev": { 27 | "orchestra/testbench": ">=6.0", 28 | "squizlabs/php_codesniffer": "^3.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Cerbero\\OctaneTestbench\\": "src" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Cerbero\\OctaneTestbench\\": "tests" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "phpunit", 42 | "check-style": "phpcs --standard=PSR12 src tests", 43 | "fix-style": "phpcbf --standard=PSR12 src tests" 44 | }, 45 | "extra": { 46 | "branch-alias": { 47 | "dev-master": "1.0-dev" 48 | } 49 | }, 50 | "config": { 51 | "sort-packages": true 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Concerns/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | getBasePath()); 23 | $bindings = $this->getInitialServerBindings(); 24 | $app = $factory->warm($factory->createApplication($bindings)); 25 | 26 | $app->events->forget(RequestReceived::class); 27 | 28 | return $app; 29 | } 30 | 31 | /** 32 | * Retrieve the application base path. 33 | * 34 | * @return string 35 | */ 36 | protected function getBasePath(): string 37 | { 38 | return realpath(dirname(__DIR__, 5)); 39 | } 40 | 41 | /** 42 | * Retrieve the initial services to bind for the server in use 43 | * 44 | * @return array 45 | */ 46 | protected function getInitialServerBindings(): array 47 | { 48 | $serverState = []; 49 | $serverState['octaneConfig'] = require $this->getBasePath() . '/config/octane.php'; 50 | 51 | if ($serverState['octaneConfig']['server'] == 'roadrunner') { 52 | return []; 53 | } 54 | 55 | $workerState = (object) ['tables' => require $this->getOctaneBinPath() . '/createSwooleTables.php']; 56 | 57 | return [ 58 | 'octane.cacheTable' => require $this->getOctaneBinPath() . '/createSwooleCacheTable.php', 59 | 'Swoole\Http\Server' => Mockery::spy('Swoole\Http\Server'), 60 | 'Laravel\Octane\Swoole\WorkerState' => $workerState, 61 | ]; 62 | } 63 | 64 | /** 65 | * Retrieve the Octane bin path. 66 | * 67 | * @return string 68 | */ 69 | protected function getOctaneBinPath(): string 70 | { 71 | return $this->getBasePath() . '/vendor/laravel/octane/bin'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithCache.php: -------------------------------------------------------------------------------- 1 | app->cache->store('octane'); 21 | 22 | if ($value === null) { 23 | $this->assertTrue($cache->has($key), "The Octane cache doesn't have the [$key] key set"); 24 | } else { 25 | $actual = $cache->get($key); 26 | $this->assertSame($value, $actual, "Expected cached value [$value], got [$actual] instead"); 27 | } 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * Assert that the Laravel Octane cache doesn't have the given key set 34 | * 35 | * @param string $key 36 | * @return static 37 | */ 38 | public function assertOctaneCacheMissing(string $key): static 39 | { 40 | $cache = $this->app->cache->store('octane'); 41 | 42 | $this->assertFalse($cache->has($key), "The Octane cache has the [$key] key set"); 43 | 44 | return $this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithContainer.php: -------------------------------------------------------------------------------- 1 | app[$target], ...$parameters); 23 | 24 | $this->app->instance($target, $mock); 25 | 26 | return $this; 27 | } 28 | 29 | /** 30 | * Partially mock the given bound target 31 | * 32 | * @param string $target 33 | * @param mixed ...$parameters 34 | * @return static 35 | */ 36 | public function partiallyMocks(string $target, ...$parameters): static 37 | { 38 | $mock = Mockery::mock($this->app[$target], ...$parameters)->makePartial(); 39 | 40 | $this->app->instance($target, $mock); 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithEvents.php: -------------------------------------------------------------------------------- 1 | app->events->listen($event, $listener); 22 | } 23 | 24 | return $this; 25 | } 26 | 27 | /** 28 | * Exclude listeners for the given event 29 | * 30 | * @param string $event 31 | * @param mixed ...$listeners 32 | * @return static 33 | */ 34 | public function stopsListeningTo(string $event, mixed ...$listeners): static 35 | { 36 | $this->app->events->forget($event); 37 | 38 | $listeners = array_diff($this->app->config['octane.listeners'][$event], $listeners); 39 | 40 | return $this->listensTo($event, ...$listeners); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithTables.php: -------------------------------------------------------------------------------- 1 | exist($row); 25 | $this->assertTrue($actual, "The row [$row] is not present in the Octane table [$table]"); 26 | } else { 27 | [$row, $column] = explode('.', $row); 28 | $actual = Octane::table($table)->get($row, $column); 29 | $this->assertSame($value, $actual, "Expected value [$value] in column [$column], got [$actual] instead"); 30 | } 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Assert that the given Laravel Octane table doesn't have the provided row 37 | * 38 | * @param string $table 39 | * @param string $row 40 | * @return static 41 | */ 42 | public function assertOctaneTableMissing(string $table, string $row): static 43 | { 44 | $actual = Octane::table($table)->exist($row); 45 | 46 | $this->assertFalse($actual, "The row [$row] is present in the Octane table [$table]"); 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * Assert that the given Laravel Octane table contains the provided number of rows 53 | * 54 | * @param string $table 55 | * @param int $count 56 | * @return static 57 | */ 58 | public function assertOctaneTableCount(string $table, int $count): static 59 | { 60 | $actual = Octane::table($table)->count(); 61 | 62 | $this->assertSame($count, $actual, "The rows in the Octane table [$table] are $actual"); 63 | 64 | return $this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Concerns/MakesHttpRequests.php: -------------------------------------------------------------------------------- 1 | prepareUrlForRequest($uri), 35 | $method, 36 | $parameters, 37 | $cookies, 38 | array_merge($files, $this->extractFilesFromDataArray($parameters)), 39 | array_replace($this->serverVariables, $server), 40 | $content, 41 | ); 42 | 43 | return $this->sendToOctane(Request::createFromBase($request)); 44 | } 45 | 46 | /** 47 | * Send a request to Laravel Octane. 48 | * 49 | * @param Request $request 50 | * @return ResponseTestCase 51 | */ 52 | public function sendToOctane(Request $request): ResponseTestCase 53 | { 54 | $factory = new ApplicationFactoryStub($this->app); 55 | $client = new ClientStub($this); 56 | $worker = new WorkerStub($factory, $client); 57 | 58 | $this->app->instance(Client::class, $client); 59 | 60 | $worker->boot(); 61 | 62 | return $worker->runRequest($request); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Concerns/ProvidesConcurrencySupport.php: -------------------------------------------------------------------------------- 1 | mocks('octane', function ($mock) use ($callback) { 25 | $mock->shouldReceive('concurrently')->andReturnUsing($callback ?: function (array $tasks) { 26 | array_walk($tasks, fn (&$task) => $task = $task()); 27 | return $tasks; 28 | }); 29 | }); 30 | } 31 | 32 | /** 33 | * Expect Octane to run tasks concurrently 34 | * 35 | * @param array ...$results 36 | * @return static 37 | */ 38 | public function expectsConcurrencyResults(array ...$results): static 39 | { 40 | return $this->mocks('octane', function ($mock) use ($results) { 41 | $mock->shouldReceive('concurrently')->andReturn(...$results); 42 | }); 43 | } 44 | 45 | /** 46 | * Expect Octane to throw an exception when running tasks concurrently 47 | * 48 | * @param Throwable|null $e 49 | * @return static 50 | */ 51 | public function expectsConcurrencyException(Throwable $e = null): static 52 | { 53 | $exception = TaskExceptionResult::from($e ?: new Exception())->getOriginal(); 54 | 55 | return $this->mocks('octane', function ($mock) use ($exception) { 56 | $mock->shouldReceive('concurrently')->andThrow($exception); 57 | }); 58 | } 59 | 60 | /** 61 | * Expect Octane to timeout when running tasks concurrently 62 | * 63 | * @param int $milliseconds 64 | * @return static 65 | */ 66 | public function expectsConcurrencyTimeout(int $milliseconds = 100): static 67 | { 68 | return $this->mocks('octane', function ($mock) use ($milliseconds) { 69 | $mock->shouldReceive('concurrently')->andThrow(TaskTimeoutException::after($milliseconds)); 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ResponseTestCase.php: -------------------------------------------------------------------------------- 1 | exception = $response; 36 | } else { 37 | parent::__construct($this->toIlluminateResponse($response)); 38 | } 39 | } 40 | 41 | /** 42 | * Turn the given Symfony response into an Illuminate response 43 | * 44 | * @param SymfonyResponse $response 45 | * @return Response 46 | */ 47 | protected function toIlluminateResponse(SymfonyResponse $response): Response 48 | { 49 | if ($response instanceof Response) { 50 | return $response; 51 | } 52 | 53 | return new Response( 54 | $response->getContent(), 55 | $response->getStatusCode(), 56 | $response->headers->allPreserveCase(), 57 | ); 58 | } 59 | 60 | /** 61 | * Assert that the thrown exception matches the given exception 62 | * 63 | * @param Throwable|string $exception 64 | * @return static 65 | */ 66 | public function assertException(Throwable|string $exception): static 67 | { 68 | $class = is_string($exception) ? $exception : $exception::class; 69 | 70 | Assert::assertInstanceOf($class, $this->exception); 71 | 72 | return is_string($exception) ? $this : $this->assertExceptionMessage($exception->getMessage()); 73 | } 74 | 75 | /** 76 | * Assert that the thrown exception message matches the given message 77 | * 78 | * @param string $message 79 | * @return static 80 | */ 81 | public function assertExceptionMessage(string $message): static 82 | { 83 | Assert::assertSame($message, $this->exception->getMessage()); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Handle dynamic calls into macros or pass missing methods to the response or test case. 90 | * 91 | * @param string $method 92 | * @param array $args 93 | * @return mixed 94 | */ 95 | public function __call($method, $args) 96 | { 97 | if (static::hasMacro($method)) { 98 | parent::__call($method, $args); 99 | } elseif ($this->baseResponse && method_exists($this->baseResponse, $method)) { 100 | return $this->baseResponse->$method(...$args); 101 | } else { 102 | $reflection = new ReflectionMethod($this->testCase, $method); 103 | $reflection->setAccessible(true); 104 | $reflection->invokeArgs($this->testCase, $args); 105 | } 106 | 107 | return $this; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Stubs/ApplicationFactoryStub.php: -------------------------------------------------------------------------------- 1 | basePath()); 22 | } 23 | 24 | /** 25 | * Create a new application instance. 26 | * 27 | * @param array $initialInstances 28 | * @return Application 29 | */ 30 | public function createApplication(array $initialInstances = []): Application 31 | { 32 | return $this->app; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Stubs/ClientStub.php: -------------------------------------------------------------------------------- 1 | response = new ResponseTestCase($this->testCase, $response->response); 57 | } 58 | 59 | /** 60 | * Record the error. 61 | * 62 | * @param Throwable $e 63 | * @param Application $app 64 | * @param Request $request 65 | * @param RequestContext $context 66 | * @return void 67 | */ 68 | public function error(Throwable $e, Application $app, Request $request, RequestContext $context): void 69 | { 70 | while (ob_get_level() > 1) { 71 | ob_end_clean(); 72 | } 73 | 74 | $this->response = new ResponseTestCase($this->testCase, $e); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Stubs/WorkerStub.php: -------------------------------------------------------------------------------- 1 | app->instance('request', $request); 25 | 26 | $data = $this->client->marshalRequest(new RequestContext(compact('request'))); 27 | 28 | $this->handle($data['context']->request, $data['context']); 29 | 30 | return $this->client->response; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/TestsOctaneApplication.php: -------------------------------------------------------------------------------- 1 |