illuminate/testing
Illuminate Testing is Laravel’s official testing package, offering the base TestCase plus expressive helpers, HTTP and database assertions, and fakes for events, queues, mail, and more so you can write reliable feature and unit tests quickly.
The honest first step: you almost certainly already have this code installed. illuminate/testing is a read-only subtree split of laravel/framework, so composer require laravel/framework gives you the identical source under vendor/laravel/framework/src/Illuminate/Testing. Only reach for composer require --dev illuminate/testing when you specifically want that narrow split (auditing the component, or a package that wants the minimal dependency surface).
Be aware of where the line falls: this split ships the Illuminate\Testing\* namespace — TestResponse, AssertableJsonString, Fluent\AssertableJson, TestView, TestComponent, TestComponentCollection, Exceptions\*. The base TestCase, MakesHttpRequests, and the traits you actually use (RefreshDatabase, DatabaseTransactions, WithFaker) live in Illuminate\Foundation\Testing. Day to day you extend Tests\TestCase and interact with this package through TestResponse.
Minimal path:
php artisan make:test OrderIndexTest — generates a class extending Tests\TestCase.php artisan test --filter=OrderIndexTest.Files worth reading first in the vendor tree:
Illuminate/Testing/TestResponse.php — the assertion surface you'll live in (assertOk, assertJson, assertJsonPath, assertSee, assertSessionHas, assertRedirect).Illuminate/Testing/Fluent/AssertableJson.php — the scoped fluent JSON API (has, where, etc, scope).Illuminate/Testing/AssertableJsonString.php — the same assertions against a raw JSON string, no HTTP response required.Illuminate/Testing/TestView.php / TestComponent.php — asserting on Blade output and components.First use case — assert structure, not a frozen payload:
public function test_orders_index_is_paginated(): void
{
$user = User::factory()->has(Order::factory()->count(3))->create();
$this->actingAs($user)
->getJson('/api/orders')
->assertOk()
->assertJsonPath('meta.per_page', 15)
->assertJsonCount(3, 'data');
}
Prefer scoped fluent JSON assertions over array literals. assertJson([...]) is a subset match and reads poorly once payloads grow. AssertableJson lets you pin the fields that matter and explicitly ignore the rest:
->assertJson(fn (AssertableJson $json) => $json
->has('data', 3)
->has('data.0', fn (AssertableJson $json) => $json
->where('status', 'pending')
->whereType('total', 'integer')
->etc() // tolerate extra keys, but declare it
)
->has('meta')
);
Drive console commands through $this->artisan(), which returns a PendingCommand from this ecosystem — chain expectsOutput, expectsQuestion, expectsChoice, assertExitCode. Use $this->artisan(...)->run() when you need the exit code in a variable rather than an assertion.
Boundary fakes are the highest-leverage helpers. Bus::fake(), Queue::fake(), Event::fake(), Notification::fake(), Mail::fake(), Http::fake() (the Illuminate\Support\Facades\Http client, not this package, but it is what you'll pair with TestResponse). Assert intent — Bus::assertDispatched(SendReceipt::class, fn ($job) => $job->order->is($order)) — rather than asserting side effects you don't own.
Mock and spy through the container, not new:
$this->mock(PaymentGateway::class, fn ($m) => $m
->shouldReceive('charge')->once()->andReturn(new Receipt(['id' => 'ch_1']))
);
$this->spy(Logger::class)->shouldHaveReceived('info');
Database strategy is a per-suite decision. RefreshDatabase migrates once per process and wraps each test in a transaction — the default. DatabaseTransactions skips migration and is faster when your schema is already seeded by CI. Reserve DatabaseMigrations for tests that genuinely need a clean migrate (e.g., migration tests themselves).
Register custom assertions as macros. TestResponse is Macroable, which is the clean extension point for domain-specific assertions:
// app/Providers/TestingServiceProvider.php (registered only in local/testing)
TestResponse::macro('assertOrderShipped', function () {
/** @var TestResponse $this */
return $this->assertJsonPath('data.shipped_at', fn ($v) => $v !== null);
});
For package development, depend on illuminate/testing only if you need TestResponse-typed signatures; otherwise require the framework in require-dev and let consumers bring their own.
illuminate/* splits must share a major (13.x with 13.x). A mismatch surfaces as a confusing Composer resolution failure or, worse, a missing method at runtime. In a Laravel app this is a non-issue — you get one framework version.laravel/framework. Anything you "fix" locally will be overwritten.$this->app->instance(...), facade swaps via Facade::swap(), and Config::set() persist unless the app is refreshed. Prefer $this->app->refreshApplication() in setUp for suites that mutate bindings, and remember RefreshDatabase resets the DB, not the container.assertJson is a subset match — it is not assertExactJson. Extra keys pass silently. If you need to catch accidental payload growth, say so explicitly with ->etc() in the fluent API or use assertExactJson.assertJsonStructure wildcards are shallow. ['data' => [['id', 'total']]] asserts the shape of the first element only; it does not verify every item. Use ->has('data', $count) or each() for real coverage.dump() / dd() exist on TestResponse and print the response body mid-chain, which is the fastest way to debug an assertion failure that only says "failed asserting that two strings are equal." You can also ->dumpHeaders() and ->dumpSession().withoutExceptionHandling() first, always, when a test fails opaquely. By default exceptions are rendered into a 500 response; disabling the handler surfaces the real stack trace and stack frame inside your app.Faker::create() yourself if you use WithFaker — use $this->faker so the seed stays consistent within a test.assertSee escapes by default. assertSee('A & B') will not find A & B in HTML-rendered output; pass false as the second argument or use assertSeeText.TestResponse assertions like assertJson don't apply to StreamedResponse; use $response->streamedContent() and assert on the string.$message through to assertTrue — future you, reading CI output, will thank present you.Tests\TestCase with your own assertDomainInvariant() helpers, or add traits per concern (InteractsWithBilling, FreezesTime). Keep them in tests/, not in app/, so they never ship to production.How can I help you explore Laravel packages today?