|
| 1 | +--- |
| 2 | +outline: [2, 3] |
| 3 | +llms: true |
| 4 | +llms_description: "How to test Laravel applications with Testo through the third-party ichinya/laratesto package: register LaravelPlugin with LaravelConfig in a suite, write HTTP, database and Artisan tests with LaravelTestCase or the InteractsWithLaravel trait, prepare state in setUpLaravel(), isolate the database with #[RefreshDatabase], #[DatabaseTransactions], #[DatabaseMigrations] or #[DatabaseTruncation], and convert PHPUnit tests with the ichinya/laratesto-rector migrator." |
| 5 | +--- |
| 6 | + |
| 7 | +# Laravel (Laratesto) |
| 8 | + |
| 9 | +[Laratesto](https://packagist.org/packages/ichinya/laratesto) helps you test Laravel applications with Testo. It boots a fresh Laravel application for every test and brings the helpers you know from Laravel's own testing layer: HTTP requests, authentication, session and database assertions, facades with their fakes, and Artisan commands. |
| 10 | + |
| 11 | +::: info Third-party package |
| 12 | +Laratesto is developed and maintained by a third-party developer, [ichinya](https://github.com/ichinya). Report issues and follow releases in [its repository](https://github.com/ichinya/laratesto). |
| 13 | +::: |
| 14 | + |
| 15 | +<plugin-info class="\Laratesto\LaravelPlugin" name="Laravel" github="https://github.com/ichinya/laratesto" /> |
| 16 | + |
| 17 | +## Installation |
| 18 | + |
| 19 | +Laratesto needs a Laravel 13 application and PHP 8.3 or newer. Run this in the application root: |
| 20 | + |
| 21 | +```bash |
| 22 | +composer require --dev ichinya/laratesto |
| 23 | +``` |
| 24 | + |
| 25 | +Composer pulls in a compatible version of Testo along with it. You can move over step by step: existing PHPUnit or Pest tests keep running on their own runner. |
| 26 | + |
| 27 | +## Setup |
| 28 | + |
| 29 | +Register <class>\Laratesto\LaravelPlugin</class> in the `plugins` section of the Test Suite whose tests need a running Laravel application, usually feature tests. Unit tests are better kept in a separate suite without the plugin, so an application isn't booted for each of them. If Testo isn't configured in the project yet, start with [Getting Started](../intro/getting-started.md#configuration). |
| 30 | + |
| 31 | +```php |
| 32 | +use Laratesto\Config\LaravelConfig; |
| 33 | +use Laratesto\LaravelPlugin; |
| 34 | +use Testo\Application\Config\SuiteConfig; |
| 35 | + |
| 36 | +new SuiteConfig( |
| 37 | + name: 'Laravel', |
| 38 | + location: ['tests/Feature'], |
| 39 | + plugins: [ |
| 40 | + new LaravelPlugin(new LaravelConfig( |
| 41 | + basePath: __DIR__, |
| 42 | + environment: 'testing', |
| 43 | + config: [ |
| 44 | + 'database.default' => 'sqlite', |
| 45 | + 'database.connections.sqlite.database' => ':memory:', |
| 46 | + 'cache.default' => 'array', |
| 47 | + 'session.driver' => 'array', |
| 48 | + ], |
| 49 | + )), |
| 50 | + ], |
| 51 | +); |
| 52 | +``` |
| 53 | + |
| 54 | +<signature h="3" name="new \Laratesto\Config\LaravelConfig(string $basePath, string $environment = 'testing', array $config = [])"> |
| 55 | +<short>Configures how Laratesto boots the Laravel application for each test.</short> |
| 56 | +<param name="$basePath">The application root, the directory that contains `bootstrap/app.php`.</param> |
| 57 | +<param name="$environment">Becomes `APP_ENV`. Laratesto sets it before the application boots, so Laravel loads `.env.testing` as usual.</param> |
| 58 | +<param name="$config">Overrides configuration values. They are applied after the application has booted, so a service provider that reads a setting while booting won't see the override: put such settings in `.env.testing` instead.</param> |
| 59 | +</signature> |
| 60 | + |
| 61 | +::: warning Use a throwaway database |
| 62 | +The database attributes described below run migrations and truncate tables. Point tests at a database you don't mind losing, like the in-memory SQLite from the example (it needs the PDO SQLite extension). |
| 63 | +::: |
| 64 | + |
| 65 | +## Writing tests |
| 66 | + |
| 67 | +### Extending the base class |
| 68 | + |
| 69 | +The easiest way to start is to extend <class>\Laratesto\Testing\LaravelTestCase</class>. Let's add a JSON route to `routes/web.php`: |
| 70 | + |
| 71 | +```php |
| 72 | +<?php |
| 73 | + |
| 74 | +use Illuminate\Support\Facades\Route; |
| 75 | + |
| 76 | +Route::get('/laratesto/ping', static fn() => response()->json(['message' => 'pong'])); |
| 77 | +``` |
| 78 | + |
| 79 | +And a test for it in `tests/Feature/HttpTest.php`: |
| 80 | + |
| 81 | +```php |
| 82 | +<?php |
| 83 | + |
| 84 | +declare(strict_types=1); |
| 85 | + |
| 86 | +namespace Tests\Feature; |
| 87 | + |
| 88 | +use Laratesto\Testing\LaravelTestCase; |
| 89 | +use Testo\Test; |
| 90 | + |
| 91 | +#[Test] |
| 92 | +final class HttpTest extends LaravelTestCase |
| 93 | +{ |
| 94 | + public function returnsJson(): void |
| 95 | + { |
| 96 | + $this->get('/laratesto/ping') |
| 97 | + ->assertOk() |
| 98 | + ->assertExactJson(['message' => 'pong']); |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +When the test runs, the request goes through Laravel's real HTTP kernel, without a web server. The response comes back as <class>\Laratesto\Testing\LaravelResponse</class>: its assertions look just like Laravel's, but they report to Testo. |
| 104 | + |
| 105 | +### Using the trait |
| 106 | + |
| 107 | +If your tests already extend a base class of your own, use the <class>\Laratesto\Testing\InteractsWithLaravel</class> trait instead: it gives a class the same helpers. The suite still needs the <plugin>Laravel</plugin> plugin. For example, `tests/Feature/ContainerTest.php`: |
| 108 | + |
| 109 | +```php |
| 110 | +<?php |
| 111 | + |
| 112 | +declare(strict_types=1); |
| 113 | + |
| 114 | +namespace Tests\Feature; |
| 115 | + |
| 116 | +use Laratesto\Testing\InteractsWithLaravel; |
| 117 | +use Testo\Assert; |
| 118 | +use Testo\Test; |
| 119 | + |
| 120 | +#[Test] |
| 121 | +final class ContainerTest |
| 122 | +{ |
| 123 | + use InteractsWithLaravel; |
| 124 | + |
| 125 | + public function usesTheTestingEnvironment(): void |
| 126 | + { |
| 127 | + Assert::same($this->app()->environment(), 'testing'); |
| 128 | + } |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +### Available helpers |
| 133 | + |
| 134 | +The base class and the trait provide the same set of protected helpers: |
| 135 | + |
| 136 | +- HTTP requests (`get()`, `postJson()` and others), custom headers, bearer tokens and the option to disable middleware. Cookies from a response are sent with the next request in the same test, just like in a browser. |
| 137 | +- Authentication (`actingAs()`, `assertAuthenticated()`), the session, and database assertions (`assertDatabaseHas()`, `assertModelExists()` and others). |
| 138 | +- The service container, facades and their fakes (`Queue::fake()`, `Event::fake()`). |
| 139 | +- Running Artisan commands and checking their exit code and output. |
| 140 | + |
| 141 | +Laravel's fakes check their expectations through PHPUnit assertions. Laratesto ships a small stand-in for those classes, so the fakes work without PHPUnit installed. It isn't a full PHPUnit replacement, though: there's no `createMock()`, for example. For mocks, use <plugin>Mockery</plugin>. The full list of helpers and their limitations is in the [Laratesto README](https://github.com/ichinya/laratesto#http-helpers). |
| 142 | + |
| 143 | +## Application lifecycle |
| 144 | + |
| 145 | +Every test gets a new application instance. Setup that needs the application (the container, configuration, facades) goes into the <func>\Laratesto\Testing\InteractsWithLaravel::setUpLaravel()</func> hook, and cleanup that still needs it goes into <func>\Laratesto\Testing\InteractsWithLaravel::tearDownLaravel()</func>. Here's the order of a single test: |
| 146 | + |
| 147 | +1. Laratesto boots the application and passes it to the test. |
| 148 | +2. If the test has a database attribute, the database is prepared. |
| 149 | +3. `setUpLaravel()` runs. |
| 150 | +4. The test itself runs. |
| 151 | +5. `tearDownLaravel()` runs. |
| 152 | +6. The database transaction or migrations are rolled back, and Laratesto resets Laravel's state. |
| 153 | + |
| 154 | +<signature compact name="\Laratesto\Testing\InteractsWithLaravel::setUpLaravel(): void"> |
| 155 | +<short>Protected hook called before each test, once the application is ready and the database is prepared.</short> |
| 156 | +</signature> |
| 157 | + |
| 158 | +<signature compact name="\Laratesto\Testing\InteractsWithLaravel::tearDownLaravel(): void"> |
| 159 | +<short>Protected hook called after each test, while the application is still available.</short> |
| 160 | +</signature> |
| 161 | + |
| 162 | +Keep in mind that Testo creates one instance of a test class and reuses it for all its tests (see <plugin>Lifecycle</plugin>). The constructor runs only once, so reset per-test properties in `setUpLaravel()`. |
| 163 | + |
| 164 | +::: warning One test at a time |
| 165 | +Laravel keeps a lot of global state, so application tests must run strictly one after another. If you use <attr>\Testo\Fiber\RunInFiber</attr>, keep the default <enum>\Testo\Fiber\Schedule::Solo</enum>: with <enum>\Testo\Fiber\Schedule::RoundRobin</enum> or <enum>\Testo\Fiber\Schedule::Random</enum> on a class, tests would interleave, so Laratesto stops them with an error. |
| 166 | +::: |
| 167 | + |
| 168 | +## Database isolation |
| 169 | + |
| 170 | +Laravel's `RefreshDatabase` and similar traits are built for PHPUnit. With Laratesto, use attributes with the same names from the `Laratesto\Attribute` namespace instead. You can put any of them on a test method or on the whole class. |
| 171 | + |
| 172 | +For a standard Laravel application with the default `User` model and its migration, a database test looks like this (`tests/Feature/DatabaseTest.php`): |
| 173 | + |
| 174 | +```php |
| 175 | +<?php |
| 176 | + |
| 177 | +declare(strict_types=1); |
| 178 | + |
| 179 | +namespace Tests\Feature; |
| 180 | + |
| 181 | +use App\Models\User; |
| 182 | +use Laratesto\Attribute\RefreshDatabase; |
| 183 | +use Laratesto\Testing\LaravelTestCase; |
| 184 | +use Testo\Test; |
| 185 | + |
| 186 | +#[Test] |
| 187 | +final class DatabaseTest extends LaravelTestCase |
| 188 | +{ |
| 189 | + #[RefreshDatabase] |
| 190 | + public function persistsAUser(): void |
| 191 | + { |
| 192 | + $user = User::factory()->create(); |
| 193 | + |
| 194 | + $this->assertDatabaseHas('users', ['id' => $user->getKey()]); |
| 195 | + } |
| 196 | +} |
| 197 | +``` |
| 198 | + |
| 199 | +<signature h="3" name="#[\Laratesto\Attribute\RefreshDatabase(bool $seed = false, ?string $seeder = null, bool $dropViews = false, bool $dropTypes = false, ?array $connections = null)]"> |
| 200 | +<short>Migrates the database once, then wraps each test in a transaction that is rolled back.</short> |
| 201 | +<description> |
| 202 | + |
| 203 | +The fastest option for most applications. `migrate:fresh` runs once per process (and again if the schema disappears), then every test runs inside a transaction that is rolled back afterward. |
| 204 | + |
| 205 | +</description> |
| 206 | +</signature> |
| 207 | + |
| 208 | +<signature h="3" name="#[\Laratesto\Attribute\DatabaseTransactions(?array $connections = null)]"> |
| 209 | +<short>Wraps each test in a transaction that is rolled back, without running migrations.</short> |
| 210 | +<description> |
| 211 | + |
| 212 | +Use it when the schema is already in place, for example when it is created outside the tests. |
| 213 | + |
| 214 | +</description> |
| 215 | +</signature> |
| 216 | + |
| 217 | +<signature h="3" name="#[\Laratesto\Attribute\DatabaseMigrations(bool $seed = false, ?string $seeder = null, bool $dropViews = false, bool $dropTypes = false)]"> |
| 218 | +<short>Runs `migrate:fresh` before each test and rolls the migrations back after it.</short> |
| 219 | +<description> |
| 220 | + |
| 221 | +The slowest option, but every test starts with a freshly built schema. |
| 222 | + |
| 223 | +</description> |
| 224 | +</signature> |
| 225 | + |
| 226 | +<signature h="3" name="#[\Laratesto\Attribute\DatabaseTruncation(bool $seed = false, ?string $seeder = null, bool $dropViews = false, bool $dropTypes = false, ?array $connections = null, ?array $tables = null, ?array $exceptTables = null)]"> |
| 227 | +<short>Truncates tables before each test.</short> |
| 228 | +<description> |
| 229 | + |
| 230 | +Useful when the code under test can't run inside a transaction. The first time, the database is migrated with `migrate:fresh`; after that, the tables are truncated before each test and, if requested, seeded again. Nothing is cleaned up after the test. |
| 231 | + |
| 232 | +</description> |
| 233 | +</signature> |
| 234 | + |
| 235 | +::: tip Seeding inside a test |
| 236 | +`$this->seed()` from Laravel's `TestCase` isn't available. Use the attribute's `seed` / `seeder` parameters, or call `Artisan::call('db:seed', ['--force' => true])` in the test. |
| 237 | +::: |
| 238 | + |
| 239 | +## Migrating from PHPUnit |
| 240 | + |
| 241 | +Laratesto has a companion package, [`ichinya/laratesto-rector`](https://github.com/ichinya/laratesto-rector), that converts existing Laravel PHPUnit tests to Testo. It is built on Rector and reuses the generic rules of the [Rector integration](./rector.md), adding Laravel-specific ones on top: for example, it turns the `RefreshDatabase` trait into the attribute of the same name and puts <attr>\Testo\Test</attr> on test methods. |
| 242 | + |
| 243 | +```bash |
| 244 | +composer require --dev ichinya/laratesto-rector |
| 245 | +``` |
| 246 | + |
| 247 | +Commit your tests before migrating: the migrator refuses to apply changes to files with uncommitted edits. Then run it twice, first to preview and then to apply: |
| 248 | + |
| 249 | +```bash |
| 250 | +php artisan laratesto:migrate-rector # preview: files stay untouched |
| 251 | +php artisan laratesto:migrate-rector --apply # rewrite the tests |
| 252 | +git diff -- tests |
| 253 | +``` |
| 254 | + |
| 255 | +By default, the command processes the `tests` directory; to narrow it down, pass one or more `--path` options. Some constructs can't be converted safely, for example a database trait with non-trivial configuration. The migrator leaves such spots as they are, marks them with a `laratesto-residual` comment and lists them in `laratesto-residuals.json`. Fix them by hand, review the diff and run the tests. An empty report doesn't guarantee the tests behave exactly as before, so running them is the real check. |
| 256 | + |
| 257 | +The command's exit code tells you how it went: `2` means some spots need manual work, and `1` means the migration failed. |
| 258 | + |
| 259 | +The full list of options and supported constructs is in the migrator's [migration guide](https://github.com/ichinya/laratesto-rector/blob/main/MIGRATING.md). |
| 260 | + |
| 261 | +## Skills for AI agents |
| 262 | + |
| 263 | +Laratesto ships skills for AI agents. They are installed the same way as Testo's, through [`llm/skills`](../intro/ai-agents.md#skills-for-agents). |
0 commit comments