Open-Source Wikis

/

Laravel

/

How to contribute

/

Testing

laravel/laravel

Testing

Practical patterns for writing tests against this skeleton. The structural details (suites, base classes, env overrides) are documented in Systems → Tests; this page covers the common workflows you'll actually run.

Run the full suite

composer test          # clears cached config, runs php artisan test
php artisan test       # same, without the config:clear pre-step

composer test chains php artisan config:clear --ansi @no_additional_args && php artisan test. The @no_additional_args directive (added in v13.4.0, PR #6799) prevents extra CLI arguments — like --filter=Foo — from leaking into the config:clear invocation.

If you want to pass arguments through, run php artisan test directly:

php artisan test --filter=ExampleTest
php artisan test --testsuite=Unit
php artisan test --testsuite=Feature
php artisan test --parallel
php artisan test --coverage
php artisan test --coverage --min=80     # fail if coverage drops below 80%

Choose the right base class

// Feature test — boots the framework
namespace Tests\Feature;

use Tests\TestCase;

class ProfileTest extends TestCase
{
    public function test_user_can_view_their_profile(): void
    {
        $user = \App\Models\User::factory()->create();

        $this
            ->actingAs($user)
            ->get('/profile')
            ->assertOk();
    }
}
// Unit test — does NOT boot the framework
namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class StringHelperTest extends TestCase
{
    public function test_slug_lowercases_input(): void
    {
        $this->assertSame('hello-world', slugify('Hello World'));
    }
}

The two base classes give different capabilities:

  • Tests\TestCase (extends Illuminate\Foundation\Testing\TestCase) → HTTP helpers, container, config, DB.
  • PHPUnit\Framework\TestCase → just PHPUnit. Faster, but you can't use $this->get(...), Hash::*, Cache::*, etc.

If you need to test a class that depends on the container, it goes in Feature/ — even if it's not testing an HTTP endpoint.

Database tests

The phpunit env (phpunit.xml) sets DB_CONNECTION=sqlite and DB_DATABASE=:memory:. Apply the RefreshDatabase trait so each test runs in its own transaction:

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_email_must_be_unique(): void
    {
        User::factory()->create(['email' => 'dup@example.com']);

        $this->expectException(\Illuminate\Database\UniqueConstraintViolationException::class);
        User::factory()->create(['email' => 'dup@example.com']);
    }
}

RefreshDatabase runs migrations once per test process and wraps each test in a transaction that's rolled back at the end. Slow tests can use DatabaseMigrations (drops and re-runs migrations per test) instead, but RefreshDatabase is the default for most cases.

Common fakes

use Illuminate\Support\Facades\{Mail, Queue, Storage, Event, Bus, Notification};

Mail::fake();          // intercept mail; assert with Mail::assertSent(WelcomeMail::class)
Queue::fake();         // intercept queued jobs; Queue::assertPushed(MyJob::class)
Bus::fake();           // intercept dispatched jobs (incl. chains/batches)
Storage::fake('local'); // route Storage::disk('local') to an in-memory FS
Event::fake();         // intercept events; Event::assertDispatched(UserCreated::class)
Notification::fake();  // intercept notifications

Apply the fake before the action under test. Most assertions accept a closure for fine-grained matching:

Mail::assertSent(WelcomeMail::class, fn ($mail) => $mail->user->id === $user->id);

Using the seeder

tests/TestCase.php doesn't auto-seed, but you can call the seeder explicitly:

$this->seed();                          // runs DatabaseSeeder::run()
$this->seed(\Database\Seeders\PostsSeeder::class); // runs a specific seeder

RefreshDatabase resets the DB between tests, so seeded rows don't leak.

Handling auth

$user = \App\Models\User::factory()->create();

$this
    ->actingAs($user)
    ->get('/dashboard')
    ->assertOk();

actingAs works for the default web guard. For other guards, pass the guard name as the second argument:

$this->actingAs($apiUser, 'api');

Asserting response shape

$response = $this->getJson('/api/users');

$response
    ->assertOk()
    ->assertJsonCount(3, 'data')
    ->assertJsonStructure(['data' => [['id', 'name', 'email']]])
    ->assertJsonPath('data.0.email', 'first@example.com');

For HTML pages:

$this->get('/')
    ->assertSeeText('Let\'s get started')
    ->assertViewIs('welcome');

Handling artisan commands

$this->artisan('inspire')
    ->assertSuccessful()
    ->expectsOutputToContain('');     // an em-dash in any quote attribution

For interactive commands, chain ->expectsQuestion('What is your name?', 'Alice') and ->expectsConfirmation(...) to script the conversation.

Parallel tests

php artisan test --parallel --processes=4

This requires brianium/paratest in dev (it's not in the skeleton's composer.json by default — install it explicitly if you want this). Each worker gets its own SQLite DB; the framework auto-suffixes DB_DATABASE per process. Tests that mutate global filesystem state need careful handling.

Coverage

# Requires Xdebug or PCOV
XDEBUG_MODE=coverage php artisan test --coverage

phpunit.xml's <source> block scopes coverage to app/ only — so the 1,300 lines of config don't dilute your numbers. The skeleton's app/ directory has 64 LOC; once you add controllers and services this becomes meaningful.

CI parity

CI (.github/workflows/tests.yml) does not run coverage and does not run Pint. It runs:

  1. composer install --prefer-dist --no-interaction --no-progress
  2. cp .env.example .env
  3. php artisan key:generate
  4. php artisan test

Across PHP 8.3, 8.4, 8.5. To match CI locally:

rm -rf vendor
composer install --prefer-dist --no-interaction --no-progress
cp .env.example .env.test
php artisan key:generate --env=test
php artisan test

Common gotchas

  • Tests pass locally, fail in CI. Almost always a missing PHP extension on your local machine that CI has. Look at the workflow's extensions: list (dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite).
  • Cached config breaks tests. composer test runs php artisan config:clear first for this reason. If you skip it and you've previously run php artisan config:cache, env overrides from phpunit.xml may be ignored.
  • DB_URL set in your shell. The phpunit.xml explicitly sets <env name="DB_URL" value=""/> to neutralize this — but only if PHPUnit is the env owner. Some IDEs propagate shell env vars; if your tests are talking to a real DB, check for DB_URL in your env (echo $DB_URL).

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Testing – Laravel wiki | Factory