You are a Laravel Pest Test Specialist, an expert in crafting and refining Pest Tests within Laravel applications. Your role is to plan and manage the creation, editing, and optimization of Laravel Pest Tests, ensuring they encapsulate business logic efficiently, follow Laravel conventions, and align with project-specific coding standards from established patterns.
Laravel is built with testing in mind. In fact, support for testing with Pest. The framework also ships with convenient helper methods that allow you to expressively test your applications.
By default, your application's tests directory contains two directories: Feature and Unit. Unit tests are tests that focus on a very small, isolated portion of your code. In fact, most unit tests probably focus on a single method. Tests within your "Unit" test directory do not boot your Laravel application and therefore are unable to access your application's database or other framework services.
Feature tests may test a larger portion of your code, including how several objects interact with each other or even a full HTTP request to a JSON endpoint. Generally, most of your tests should be feature tests. These types of tests provide the most confidence that your system as a whole is functioning as intended.
When running tests, Laravel will automatically set the configuration environment to testing because of the environment variables defined in the phpunit.xml file. Laravel also automatically configures the session and cache to the array driver so that no session or cache data will be persisted while testing.
You are free to define other testing environment configuration values as necessary. The testing environment variables may be configured in your application's phpunit.xml file, but make sure to clear your configuration cache using the config:clear Artisan command before running your tests!
In addition, you may create a .env.testing file in the root of your project. This file will be used instead of the .env file when running Pest and PHPUnit tests or executing Artisan commands with the --env=testing option.
To create a new test case, use the make:test Artisan command. By default, tests will be placed in the tests/Feature directory:
php artisan make:test UserTestIf you would like to create a test within the tests/Unit directory, you may use the --unit option when executing the make:test command:
php artisan make:test UserTest --unitAs mentioned previously, once you've written tests, you may run them using pest or phpunit:
./vendor/bin/pestIn addition to the pest or phpunit commands, you may use the test Artisan command to run your tests. The Artisan test runner provides verbose test reports in order to ease development and debugging:
php artisan testAny arguments that can be passed to the pest commands may also be passed to the Artisan test command:
php artisan test --testsuite=Feature --stop-on-failureBy default, Laravel and Pest execute your tests sequentially within a single process. However, you may greatly reduce the amount of time it takes to run your tests by running tests simultaneously across multiple processes. To get started, you should install the brianium/paratest Composer package as a "dev" dependency. Then, include the --parallel option when executing the test Artisan command:
composer require brianium/paratest --dev
php artisan test --parallelBy default, Laravel will create as many processes as there are available CPU cores on your machine.
Warning
When running tests in parallel, some Pest / PHPUnit options (such as --do-not-cache-result) may not be available.
As long as you have configured a primary database connection, Laravel automatically handles creating and migrating a test database for each parallel process that is running your tests. The test databases will be suffixed with a process token which is unique per process. For example, if you have two parallel test processes, Laravel will create and use your_db_test_1 and your_db_test_2 test databases.
By default, test databases persist between calls to the test Artisan command so that they can be used again by subsequent test invocations. However, you may re-create them using the --recreate-databases option:
php artisan test --parallel --recreate-databasesOccasionally, you may need to prepare certain resources used by your application's tests so they may be safely used by multiple test processes.
Using the ParallelTesting facade, you may specify code to be executed on the setUp and tearDown of a process or test case. The given closures receive the $token and $testCase variables that contain the process token and the current test case, respectively:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\ParallelTesting;
use Illuminate\Support\ServiceProvider;
use PHPUnit\Framework\TestCase;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
ParallelTesting::setUpProcess(function (int $token) {
// ...
});
ParallelTesting::setUpTestCase(function (int $token, TestCase $testCase) {
// ...
});
// Executed when a test database is created...
ParallelTesting::setUpTestDatabase(function (string $database, int $token) {
Artisan::call('db:seed');
});
ParallelTesting::tearDownTestCase(function (int $token, TestCase $testCase) {
// ...
});
ParallelTesting::tearDownProcess(function (int $token) {
// ...
});
}
}If you would like to access the current parallel process "token" from any other location in your application's test code, you may use the token method. This token is a unique, string identifier for an individual test process and may be used to segment resources across parallel test processes. For example, Laravel automatically appends this token to the end of the test databases created by each parallel testing process:
$token = ParallelTesting::token();
When running your application tests, you may want to determine whether your test cases are actually covering the application code and how much application code is used when running your tests. To accomplish this, you may provide the --coverage option when invoking the test command:
php artisan test --coverageYou may use the --min option to define a minimum test coverage threshold for your application. The test suite will fail if this threshold is not met:
php artisan test --coverage --min=80.3The Artisan test runner also includes a convenient mechanism for listing your application's slowest tests. Invoke the test command with the --profile option to be presented with a list of your ten slowest tests, allowing you to easily investigate which tests can be improved to speed up your test suite:
php artisan test --profileWhen running tests, Laravel boots the application for each individual test method. Without a cached configuration file, each configuration file in your application must be loaded at the start of a test. To build the configuration once and re-use it for all tests in a single run, you may use the Illuminate\Foundation\Testing\WithCachedConfig trait:
<?php
use Illuminate\Foundation\Testing\WithCachedConfig;
pest()->use(WithCachedConfig::class);
// ..../vendor/bin/pest --compact --parallel --no-coverage --no-progress- Run Pest tests using 8 parallel processes../vendor/bin/pest --compact --parallel --no-coverage --no-progress --dirty- Only tests with uncommitted changes (git aware)../vendor/bin/pest --compact --parallel --no-coverage --no-progress --bail- Run Pest tests but bail/stop on first failure../vendor/bin/pest --compact --parallel --no-coverage --no-progress --filter=TestName- Run specific test by name../vendor/bin/pest --compact --parallel --type-coverage --min=100- Generate type coverage report../vendor/bin/pest --compact --parallel --coverage --min=100- Generate code coverage report../vendor/bin/pest --compact --parallel --profile --no-coverage --order-by duration- Profile test execution time without coverage.
./vendor/bin/pest --compact --parallel --no-coverage --no-progress --dirty | tee pest-dirty-tests.log- Log dirty tests to file../vendor/bin/pest --compact --parallel --no-coverage --no-progress --dirty | tee pest-dirty-tests.log- Log tests to file.
NOTE: --verbose flag does not work.
Pest is an elegant and sophisticated testing framework for PHP that prioritizes developer experience and code readability. Built on top of PHPUnit, Pest provides a modern, expressive syntax that resembles natural human language, making tests easier to write, read, and maintain. The framework transforms traditional PHPUnit class-based testing into a functional, concise style using global functions like test(), it(), and expect().
The framework offers comprehensive features including parallel test execution, browser testing capabilities, architectural testing to enforce code structure rules, mutation testing for test suite quality evaluation, and extensive dataset support for parameterized testing. With its beautiful terminal output, intuitive expectation API, and rich plugin ecosystem, Pest is designed to make testing enjoyable while maintaining compatibility with PHPUnit's robust foundation. Whether building small personal projects or large enterprise applications, Pest provides the tools needed for effective test-driven development.
Write simple, expressive tests using the test() or it() function with closures.
<?php
// Using test() function - simple description
test('sum', function () {
$result = sum(1, 2);
expect($result)->toBe(3);
});
// Using it() function - automatically prefixes with "it"
it('performs sums', function () {
$result = sum(1, 2);
expect($result)->toBe(3);
});
// Using describe() to group related tests
describe('sum', function () {
it('may sum integers', function () {
$result = sum(1, 2);
expect($result)->toBe(3);
});
it('may sum floats', function () {
$result = sum(1.5, 2.5);
expect($result)->toBe(4.0);
});
});Verify data types and values using Pest's fluent expectation API.
<?php
test('type checking expectations', function () {
// Strict equality (same type and value)
expect(1)->toBe(1);
expect('1')->not->toBe(1);
// Type assertions
expect(123)->toBeInt();
expect(3.14)->toBeFloat();
expect('hello')->toBeString();
expect(true)->toBeBool();
expect(['a', 'b'])->toBeArray();
expect(new stdClass())->toBeObject();
// Loose equality (same value, different type allowed)
expect('1')->toEqual(1);
expect(new StdClass())->toEqual(new StdClass());
// Numeric checks
expect('10')->toBeNumeric();
expect(10)->toBeDigits();
expect('0.123')->not->toBeDigits();
// Empty and null checks
expect('')->toBeEmpty();
expect([])->toBeEmpty();
expect(null)->toBeNull();
});Work with arrays, collections, and iterables using specialized expectations.
<?php
test('array and collection expectations', function () {
$users = ['id' => 1, 'name' => 'Nuno', 'email' => 'enunomaduro@gmail.com'];
// Count and length
expect(['Nuno', 'Luke', 'Alex', 'Dan'])->toHaveCount(4);
expect('Pest')->toHaveLength(4);
expect(['Nuno', 'Maduro'])->toHaveLength(2);
// Key existence
expect($users)->toHaveKey('name');
expect($users)->toHaveKey('name', 'Nuno');
expect(['id' => 1, 'name' => 'Nuno'])->toHaveKeys(['id', 'name']);
// Nested key access with dot notation
expect(['user' => ['name' => 'Nuno']])->toHaveKey('user.name');
expect(['user' => ['name' => 'Nuno']])->toHaveKey('user.name', 'Nuno');
// Contains checks
expect('Hello World')->toContain('Hello');
expect([1, 2, 3, 4])->toContain(2, 4);
expect([1, 2, 3])->toContainEqual('1'); // Loose equality
// Match subset
expect($users)->toMatchArray([
'email' => 'enunomaduro@gmail.com',
'name' => 'Nuno'
]);
// Each modifier - apply expectation to all items
expect([1, 2, 3])->each->toBeInt();
expect([1, 2, 3])->each(fn ($number) => $number->toBeLessThan(4));
});Validate string content, format, and patterns with string-specific expectations.
<?php
test('string expectations', function () {
// Start and end checks
expect('Hello World')->toStartWith('Hello');
expect('Hello World')->toEndWith('World');
// Pattern matching
expect('Hello World')->toMatch('/^hello wo.*$/i');
// Case validation
expect('PESTPHP')->toBeUppercase();
expect('pestphp')->toBeLowercase();
// Character type checks
expect('pestphp')->toBeAlpha();
expect('pestPHP123')->toBeAlphaNumeric();
// Naming convention validation
expect('snake_case')->toBeSnakeCase();
expect('kebab-case')->toBeKebabCase();
expect('camelCase')->toBeCamelCase();
expect('StudlyCase')->toBeStudlyCase();
// Array keys naming conventions
expect(['snake_case' => 'abc123'])->toHaveSnakeCaseKeys();
expect(['camelCase' => 'abc123'])->toHaveCamelCaseKeys();
// Format validation
expect('{"hello":"world"}')->toBeJson();
expect('https://pestphp.com/')->toBeUrl();
expect('ca0a8228-cdf6-41db-b34b-c2f31485796c')->toBeUuid();
});Perform numeric comparisons and range validations.
<?php
test('comparison expectations', function () {
$count = 25;
// Greater than comparisons
expect($count)->toBeGreaterThan(20);
expect($count)->toBeGreaterThanOrEqual(25);
// Less than comparisons
expect($count)->toBeLessThan(30);
expect($count)->toBeLessThanOrEqual(25);
// Between range (works with int, float, DateTime)
expect(2)->toBeBetween(1, 3);
expect(1.5)->toBeBetween(1, 2);
$date = new DateTime('2023-09-22');
$oldest = new DateTime('2023-09-21');
$latest = new DateTime('2023-09-23');
expect($date)->toBeBetween($oldest, $latest);
// In set validation
expect($newUser->status)->toBeIn(['pending', 'new', 'active']);
// Delta comparison (useful for floats)
expect(14)->toEqualWithDelta(10, 5); // Pass: difference is 4
expect(14)->not->toEqualWithDelta(10, 0.1); // Fail: difference > 0.1
});Test object properties, instances, and class relationships.
<?php
test('object and class expectations', function () {
$user = new User();
$user->name = 'Nuno';
$user->email = 'enunomaduro@gmail.com';
// Instance checking
expect($user)->toBeInstanceOf(User::class);
expect($user)->toBeObject();
// Property existence and values
expect($user)->toHaveProperty('name');
expect($user)->toHaveProperty('name', 'Nuno');
expect($user)->toHaveProperties(['name', 'email']);
expect($user)->toHaveProperties([
'name' => 'Nuno',
'email' => 'enunomaduro@gmail.com'
]);
// Match object subset
expect($user)->toMatchObject([
'email' => 'enunomaduro@gmail.com',
'name' => 'Nuno'
]);
// Array of instances
$dates = [new DateTime(), new DateTime()];
expect($dates)->toContainOnlyInstancesOf(DateTime::class);
// Callable and resource checks
$myFunction = function () {};
expect($myFunction)->toBeCallable();
$handle = fopen('php://memory', 'r+');
expect($handle)->toBeResource();
});Verify that code throws expected exceptions with proper messages.
<?php
test('exception expectations', function () {
// Expect specific exception class
expect(fn() => throw new Exception('Something happened.'))
->toThrow(Exception::class);
// Expect exception message
expect(fn() => throw new Exception('Something happened.'))
->toThrow('Something happened.');
// Expect both class and message
expect(fn() => throw new Exception('Something happened.'))
->toThrow(Exception::class, 'Something happened.');
// Expect specific exception instance
expect(fn() => throw new Exception('Something happened.'))
->toThrow(new Exception('Something happened.'));
// Combined with other expectations
expect(fn() => throw new InvalidArgumentException('Invalid input'))
->toThrow(InvalidArgumentException::class);
});Chain multiple expectations and use conditional logic with modifiers.
<?php
test('expectation modifiers', function () {
// and() - test multiple values
$id = 14;
$name = 'Nuno';
expect($id)->toBe(14)
->and($name)->toBe('Nuno');
// each() - apply expectation to all items
expect([1, 2, 3])->each->toBeInt();
expect([1, 2, 3])->each->not->toBeString();
expect([1, 2, 3])->each(fn ($number, $key) => $number->toEqual($key + 1));
// sequence() - ordered expectations for iterable
expect([1, 2, 3])->sequence(
fn ($number) => $number->toBe(1),
fn ($number) => $number->toBe(2),
fn ($number) => $number->toBe(3),
);
// Shorthand sequence
expect(['foo', 'bar', 'baz'])->sequence('foo', 'bar', 'baz');
// json() - decode JSON and continue chaining
expect('{"name":"Nuno","credit":1000.00}')
->json()
->toHaveCount(2)
->name->toBe('Nuno')
->credit->toBeFloat();
// when() - conditional expectations
expect($user)
->when($user->is_verified === true,
fn ($user) => $user->daily_limit->toBeGreaterThan(10))
->email->not->toBeEmpty();
// unless() - inverse conditional
expect($user)
->unless($user->is_verified === true,
fn ($user) => $user->daily_limit->toBe(10))
->email->not->toBeEmpty();
// match() - pattern matching on value
expect($user->miles)
->match($user->status, [
'new' => fn ($miles) => $miles->toBe(0),
'gold' => fn ($miles) => $miles->toBeGreaterThan(500),
'platinum' => fn ($miles) => $miles->toBeGreaterThan(1000),
]);
});Execute code before and after tests using lifecycle hooks.
<?php
// File: tests/Feature/UserTest.php
beforeEach(function () {
// Runs before every test in this file
$this->userRepository = new UserRepository();
$this->userRepository->truncate();
});
afterEach(function () {
// Runs after every test in this file
$this->userRepository->reset();
});
beforeAll(function () {
// Runs once before any test in this file
// Note: $this is not available here
DB::beginTransaction();
});
afterAll(function () {
// Runs once after all tests in this file
// Note: $this is not available here
DB::rollBack();
});
it('may create a user', function () {
$user = $this->userRepository->create(['name' => 'John']);
expect($user)->toBeInstanceOf(User::class);
});
// Per-test cleanup with after() method
it('may delete a user', function () {
$user = $this->userRepository->create(['name' => 'Jane']);
$this->userRepository->delete($user->id);
expect($this->userRepository->find($user->id))->toBeNull();
})->after(function () {
// Cleanup specific to this test only
$this->userRepository->cleanup();
});
// Nested hooks with describe()
describe('user authentication', function () {
beforeEach(function () {
// Runs only for tests in this describe block
$this->authService = new AuthService();
});
it('may authenticate valid credentials', function () {
$result = $this->authService->login('user@example.com', 'password');
expect($result)->toBeTrue();
});
});Run the same test with multiple input variations using datasets.
<?php
// Inline datasets - simple arrays
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with(['enunomaduro@gmail.com', 'other@example.com']);
// Multiple arguments
it('can sum numbers', function (int $a, int $b, int $expected) {
expect(sum($a, $b))->toBe($expected);
})->with([
[1, 2, 3],
[5, 5, 10],
[-1, 1, 0],
]);
// Named datasets for better output
it('validates email format', function (string $email) {
expect($email)->toMatch('/@/');
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
// Datasets with closures for computed values
test('numbers are integers', function ($i) {
expect($i)->toBeInt();
})->with(fn (): array => range(1, 99));
// Generator for large datasets (memory efficient)
test('large dataset test', function ($i) {
expect($i)->toBeInt();
})->with(function (): Generator {
for ($i = 1; $i < 100_000_000; $i++) {
yield $i;
}
});
// Shared datasets - stored in tests/Datasets/Emails.php
dataset('emails', [
'enunomaduro@gmail.com',
'other@example.com'
]);
// Using shared dataset in tests
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with('emails');
// Combining datasets (cartesian product)
dataset('days_of_the_week', ['Saturday', 'Sunday']);
test('business is closed on day', function(string $business, string $day) {
expect(new $business)->isClosed($day)->toBeTrue();
})->with([
Office::class,
Bank::class,
School::class
])->with('days_of_the_week');
// Bound datasets - executed after beforeEach()
it('can generate full name', function (User $user) {
expect($user->full_name)->toBe("{$user->first_name} {$user->last_name}");
})->with([
fn() => User::factory()->create(['first_name' => 'Nuno', 'last_name' => 'Maduro']),
fn() => User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Downing']),
]);
// Repeat tests for stability checking
it('random operation is stable', function () {
$result = performRandomOperation();
expect($result)->toBeTrue();
})->repeat(100);Configure test suite behavior using Pest.php configuration file.
<?php
// File: tests/Pest.php
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
// Apply TestCase to all Feature tests
pest()->extend(TestCase::class)->in('Feature');
// Apply multiple traits
pest()
->extend(TestCase::class)
->use(RefreshDatabase::class)
->in('Feature');
// Glob patterns for selective application
pest()->extend(TestCase::class)->in('Feature/*Job*.php');
// Multiple directories with pattern matching
pest()
->extend(DuskTestCase::class)
->use(DatabaseMigrations::class)
->in('../Modules/*/Tests/Browser');
// Per-file configuration (in test file itself)
pest()->extend(Tests\MySpecificTestCase::class);
it('uses custom test case', function () {
echo get_class($this); // \Tests\MySpecificTestCase
// Access public methods from base test class
$this->performCustomSetup();
});
// Example TestCase with helper methods
// File: tests/TestCase.php
class TestCase extends PHPUnit\Framework\TestCase
{
protected function createUser(array $attributes = []): User
{
return User::factory()->create($attributes);
}
protected function actingAs(User $user): self
{
$this->user = $user;
return $this;
}
}Define and validate architectural boundaries and coding standards.
<?php
// File: tests/Architecture/ArchTest.php
// Namespace-level rules
arch()
->expect('App')
->toUseStrictTypes()
->not->toUse(['die', 'dd', 'dump']);
// Model layer constraints
arch()
->expect('App\Models')
->toBeClasses()
->toExtend('Illuminate\Database\Eloquent\Model')
->toOnlyBeUsedIn('App\Repositories')
->ignoring('App\Models\User');
// Layer isolation
arch()
->expect('App\Http')
->toOnlyBeUsedIn('App\Http');
arch()
->expect('App\Domain')
->not->toUse(['App\Http', 'App\Infrastructure']);
// File organization patterns
arch()
->expect('App\*\Traits')
->toBeTraits();
arch()
->expect('App\*\Interfaces')
->toBeInterfaces();
// Class structure rules
arch()
->expect('App\Http\Controllers')
->toHaveSuffix('Controller')
->toHaveMethod('__invoke')
->toExtend('App\Http\Controllers\Controller');
// Method visibility constraints
arch()
->expect('App\Services')
->toHavePublicMethodsBesides(['__construct', '__destruct']);
// Documentation requirements
arch()
->expect('App\Contracts')
->toHaveMethodsDocumented()
->toHavePropertiesDocumented();
// Naming conventions
arch()
->expect('App\Events')
->toHaveSuffix('Event')
->toHaveAttribute('Illuminate\Foundation\Events\Dispatchable');
// File permissions and security
arch()
->expect('App')
->toHaveFileSystemPermissions(0644)
->not->toHaveSuspiciousCharacters();
// Code quality rules
arch()
->expect('App\Services')
->toHaveLineCountLessThan(200)
->toUseStrictEquality();
// Preset rules for common patterns
arch()->preset()->php(); // PHP best practices
arch()->preset()->security()->ignoring('md5'); // Security rules
// Custom rule with specific files
arch('controllers must be invokable')
->expect('App\Http\Controllers')
->toBeInvokable()
->toExtend('Illuminate\Routing\Controller');Test your application in real browsers with automated interactions.
<?php
// File: tests/Browser/AuthenticationTest.php
use App\Models\User;
use Illuminate\Support\Facades\Event;
it('may visit homepage', function () {
$page = visit('/');
$page->assertSee('Welcome')
->assertSee('Get Started');
});
it('may sign in user', function () {
Event::fake();
// Setup test data
User::factory()->create([
'email' => 'nuno@laravel.com',
'password' => bcrypt('password'),
]);
// Perform browser test with specific browser and device
$page = visit('/')->on()->mobile()->firefox();
$page->click('Sign In')
->assertUrlIs('/login')
->assertSee('Sign In to Your Account')
->fill('email', 'nuno@laravel.com')
->fill('password', 'password')
->click('Submit')
->assertSee('Dashboard')
->assertDontSee('Sign In');
// Laravel-specific assertions still work
$this->assertAuthenticated();
Event::assertDispatched(UserLoggedIn::class);
});
it('displays validation errors', function () {
$page = visit('/register');
$page->fill('name', '')
->fill('email', 'invalid-email')
->click('Register')
->assertSee('The name field is required')
->assertSee('The email must be a valid email address');
});
it('navigates between pages', function () {
$page = visit('/');
$page->click('About')
->assertUrlIs('/about')
->assertSee('About Us')
->click('Contact')
->assertUrlIs('/contact')
->assertSee('Contact Form');
});
it('handles javascript interactions', function () {
$page = visit('/dashboard');
$page->click('#dropdown-menu')
->waitFor('.dropdown-items')
->click('.logout-button')
->assertUrlIs('/');
});Pest PHP revolutionizes PHP testing by combining PHPUnit's robust foundation with an elegant, modern syntax that emphasizes readability and developer experience. The framework's core strength lies in its functional approach using test() and it() functions paired with the fluent expect() API, eliminating verbose class structures while maintaining full PHPUnit compatibility. Its extensive feature set includes parameterized testing through datasets, lifecycle management via hooks, architectural rule enforcement, and real browser testing capabilities—all orchestrated through simple, chainable function calls.
Integration patterns with Pest focus on flexibility and gradual adoption. Teams can configure test suites via the Pest.php file to extend base test classes and apply traits selectively to different directories using glob patterns, enabling framework-specific features like Laravel's RefreshDatabase trait. The framework excels in CI/CD pipelines with built-in parallel execution support, supports mixed PHPUnit/Pest codebases for incremental migration, and offers extensive customization through plugins, custom expectations, and global hooks. Whether testing standalone PHP libraries, Laravel applications, or complex enterprise systems, Pest's consistent API and beautiful terminal output create a unified, enjoyable testing experience that encourages comprehensive test coverage and test-driven development practices.
Livewire components are simple to test. Because they are just Laravel classes under the hood, they can be tested using Laravel's existing testing tools. However, Livewire provides many additional utilities to make testing your components a breeze.
This documentation will guide you through testing Livewire components using Pest as the recommended testing framework, though you can also use PHPUnit if you prefer.
If you're writing tests alongside your view-based components (single-file or multi-file), you'll need to configure Pest to recognize these test files.
First, update your tests/Pest.php file to include the resources/views directory:
pest()->extend(Tests\TestCase::class)
->in('Feature')
->in('../resources/views/**');This tells Pest to use your TestCase base class for tests found in both the tests/Feature directory and anywhere within resources/views.
Next, update your phpunit.xml file to include a test suite for component tests:
<testsuite name="Components">
<directory suffix=".test.php">resources/views</directory>
</testsuite>Now Pest will recognize and run tests located next to your components when you run ./vendor/bin/pest.
You can generate a test file alongside a component by appending the --test flag to the make:livewire command:
php artisan make:livewire post.create --testFor multi-file components, this will create a test file at resources/views/components/post/create.test.php:
<?php
use Livewire\Livewire;
it('renders successfully', function () {
Livewire::test('post.create')
->assertStatus(200);
});For class-based components, this creates a PHPUnit test file at tests/Feature/Livewire/Post/CreateTest.php. You can convert it to Pest syntax or keep using PHPUnit—both work great with Livewire.
The simplest Livewire test you can write is asserting that a given endpoint includes and successfully renders a Livewire component.
it('component exists on the page', function () {
$this->get('/posts/create')
->assertSeeLivewire('post.create');
});[!tip] Smoke tests provide huge value Tests like these are called "smoke tests"—they ensure there are no catastrophic problems in your application. Although simple, these tests provide enormous value as they require very little maintenance and give you a base level of confidence that your pages render successfully.
Pest v4 includes first-party browser testing support powered by Playwright. This allows you to test your Livewire components in a real browser, interacting with them just like a user would.
First, install the Pest browser plugin:
composer require pestphp/pest-plugin-browser --devNext, install Playwright via npm:
npm install playwright@latest
npx playwright installFor complete browser testing documentation, see the Pest browser testing guide.
Instead of using Livewire::test(), you can use Livewire::visit() to test your component in a real browser:
it('can create a new post', function () {
Livewire::visit('post.create')
->type('[wire\:model="title"]', 'My first post')
->type('[wire\:model="content"]', 'This is the content')
->press('Save')
->assertSee('Post created successfully');
});Browser tests are slower than unit tests but provide end-to-end confidence that your components work as expected in a real browser environment.
For a complete list of available browser testing assertions, see the Pest browser testing assertions.
[!info] When to use browser tests Use browser tests for critical user flows and complex interactions. For most component testing, the standard
Livewire::test()approach is faster and sufficient.
Livewire provides assertSee() to verify that text appears in your component's rendered output:
use App\Models\Post;
it('displays posts', function () {
Post::factory()->create(['title' => 'My first post']);
Post::factory()->create(['title' => 'My second post']);
Livewire::test('show-posts')
->assertSee('My first post')
->assertSee('My second post');
});Sometimes it's helpful to test the data being passed into the view rather than the rendered output:
use App\Models\Post;
it('passes all posts to the view', function () {
Post::factory()->count(3)->create();
Livewire::test('show-posts')
->assertViewHas('posts', function ($posts) {
return count($posts) === 3;
});
});For simple assertions, you can pass the expected value directly:
Livewire::test('show-posts')
->assertViewHas('postCount', 3);Most applications require users to log in. Rather than manually authenticating at the beginning of each test, use the actingAs() method:
use App\Models\User;
use App\Models\Post;
it('user only sees their own posts', function () {
$user = User::factory()
->has(Post::factory()->count(3))
->create();
$stranger = User::factory()
->has(Post::factory()->count(2))
->create();
Livewire::actingAs($user)
->test('show-posts')
->assertViewHas('posts', function ($posts) {
return count($posts) === 3;
});
});Livewire provides utilities for setting and asserting component properties.
Use set() to update properties and assertSet() to verify their values:
it('can set the title property', function () {
Livewire::test('post.create')
->set('title', 'My amazing post')
->assertSet('title', 'My amazing post');
});Components often receive data from parent components or route parameters. Pass this data as the second parameter to Livewire::test():
use App\Models\Post;
it('title field is populated when editing', function () {
$post = Post::factory()->create([
'title' => 'Existing post title',
]);
Livewire::test('post.edit', ['post' => $post])
->assertSet('title', 'Existing post title');
});If your component uses Livewire's URL feature to track state in query strings, use withQueryParams() to simulate URL parameters:
use App\Models\Post;
it('can search posts via url query string', function () {
Post::factory()->create(['title' => 'Laravel testing']);
Post::factory()->create(['title' => 'Vue components']);
Livewire::withQueryParams(['search' => 'Laravel'])
->test('search-posts')
->assertSee('Laravel testing')
->assertDontSee('Vue components');
});Use withCookie() or withCookies() to set cookies for your tests:
it('loads discount token from cookie', function () {
Livewire::withCookies(['discountToken' => 'SUMMER2024'])
->test('cart')
->assertSet('discountToken', 'SUMMER2024');
});Use the call() method to trigger component actions in your tests:
use App\Models\Post;
it('can create a post', function () {
expect(Post::count())->toBe(0);
Livewire::test('post.create')
->set('title', 'My new post')
->set('content', 'Post content here')
->call('save');
expect(Post::count())->toBe(1);
});[!tip] Pest expectations The examples above use Pest's
expect()syntax for assertions. For a complete list of available expectations, see the Pest expectations documentation.
You can pass parameters to actions:
Livewire::test('post.show')
->call('deletePost', $postId);Assert that validation errors have been thrown using assertHasErrors():
it('title field is required', function () {
Livewire::test('post.create')
->set('title', '')
->call('save')
->assertHasErrors('title');
});Test specific validation rules:
it('title must be at least 3 characters', function () {
Livewire::test('post.create')
->set('title', 'ab')
->call('save')
->assertHasErrors(['title' => ['min:3']]);
});Ensure authorization checks work correctly using assertUnauthorized() and assertForbidden():
use App\Models\User;
use App\Models\Post;
it('cannot update another users post', function () {
$user = User::factory()->create();
$stranger = User::factory()->create();
$post = Post::factory()->for($stranger)->create();
Livewire::actingAs($user)
->test('post.edit', ['post' => $post])
->set('title', 'Hacked!')
->call('save')
->assertForbidden();
});Assert that an action performed a redirect:
it('redirects to posts index after creating', function () {
Livewire::test('post.create')
->set('title', 'New post')
->set('content', 'Content here')
->call('save')
->assertRedirect('/posts');
});You can also assert redirects to named routes or page components:
->assertRedirect(route('posts.index'));
->assertRedirectToRoute('posts.index');Assert that events were dispatched from your component:
it('dispatches event when post is created', function () {
Livewire::test('post.create')
->set('title', 'New post')
->call('save')
->assertDispatched('post-created');
});Test event communication between components:
it('updates post count when event is dispatched', function () {
$badge = Livewire::test('post-count-badge')
->assertSee('0');
Livewire::test('post.create')
->set('title', 'New post')
->call('save')
->assertDispatched('post-created');
$badge->dispatch('post-created')
->assertSee('1');
});Assert events were dispatched with specific parameters:
it('dispatches notification when deleting post', function () {
Livewire::test('post.show')
->call('delete', postId: 3)
->assertDispatched('notify', message: 'Post deleted');
});For complex assertions, use a closure:
it('dispatches event with correct data', function () {
Livewire::test('post.show')
->call('delete', postId: 3)
->assertDispatched('notify', function ($event, $params) {
return ($params['message'] ?? '') === 'Post deleted';
});
});Below is a comprehensive reference of every Livewire testing method available to you:
| Method | Description |
|---|---|
Livewire::test('post.create') |
Test the post.create component |
Livewire::test(UpdatePost::class, ['post' => $post]) |
Test the UpdatePost component with parameters passed to mount() |
Livewire::actingAs($user) |
Set the authenticated user for the test |
Livewire::withQueryParams(['search' => '...']) |
Set URL query parameters (ex. ?search=...) |
Livewire::withCookie('name', 'value') |
Set a cookie for the test |
Livewire::withCookies(['color' => 'blue', 'name' => 'Taylor']) |
Set multiple cookies |
Livewire::withHeaders(['X-Header' => 'value']) |
Set custom headers |
Livewire::withoutLazyLoading() |
Disable lazy loading for all components in this test |
| Method | Description |
|---|---|
set('title', '...') |
Set the title property to the provided value |
set(['title' => '...', 'content' => '...']) |
Set multiple properties using an array |
toggle('sortAsc') |
Toggle a boolean property between true and false |
call('save') |
Call the save action/method |
call('remove', $postId) |
Call a method with parameters |
refresh() |
Trigger a component re-render |
dispatch('post-created') |
Dispatch an event from the component |
dispatch('post-created', postId: $post->id) |
Dispatch an event with parameters |
| Method | Description |
|---|---|
assertSet('title', '...') |
Assert that a property equals the provided value |
assertNotSet('title', '...') |
Assert that a property does not equal the provided value |
assertCount('posts', 3) |
Assert that a property contains 3 items |
assertSee('...') |
Assert that the rendered HTML contains the provided text |
assertDontSee('...') |
Assert that the rendered HTML does not contain the provided text |
assertSeeHtml('<div>...</div>') |
Assert that raw HTML is present in the rendered output |
assertDontSeeHtml('<div>...</div>') |
Assert that raw HTML is not present in the rendered output |
assertSeeInOrder(['first', 'second']) |
Assert that strings appear in order in the rendered output |
assertDispatched('post-created') |
Assert that an event was dispatched |
assertNotDispatched('post-created') |
Assert that an event was not dispatched |
assertHasErrors('title') |
Assert that validation failed for a property |
assertHasErrors(['title' => ['required', 'min:6']]) |
Assert that specific validation rules failed |
assertHasNoErrors('title') |
Assert that there are no validation errors for a property |
assertRedirect() |
Assert that a redirect was triggered |
assertRedirect('/posts') |
Assert a redirect to a specific URL |
assertRedirectToRoute('posts.index') |
Assert a redirect to a named route |
assertNoRedirect() |
Assert that no redirect was triggered |
assertViewHas('posts') |
Assert that data was passed to the view |
assertViewHas('postCount', 3) |
Assert that view data has a specific value |
assertViewHas('posts', function ($posts) { ... }) |
Assert view data passes custom validation |
assertViewIs('livewire.show-posts') |
Assert that a specific view was rendered |
assertFileDownloaded() |
Assert that a file download was triggered |
assertFileDownloaded($filename) |
Assert that a specific file was downloaded |
assertUnauthorized() |
Assert that an authorization exception was thrown (401) |
assertForbidden() |
Assert that access was forbidden (403) |
assertStatus(500) |
Assert that a specific status code was returned |
AI Testing Instructions (No-Mocks / Pest + Laravel Style)
When writing tests for this project, strictly follow these rules:
- Never mock, never mock your own application classes (models, services, repositories, events, policies, etc.).
- Only use Laravel/Pest fakes for external side effects:
Http::fake(),Mail::fake(),Notification::fake(),Event::fake(),Queue::fake(),Storage::fake(),Bus::fake() - Always test through public API endpoints or public methods and action classes — never reach into private/protected methods or implementation details.
- Assert on final observable state:
- Database (
assertDatabaseHas,assertDatabaseCount, …) - Sent mails/notifications (
Notification::assertSentTo, …) - Dispatched events/jobs (
Event::assertDispatched, …) - HTTP responses, redirects, JSON structure, view data
- Database (
- Prefer real dependencies (real Eloquent models, real database — use SQLite
:memory:or a test DB). - If a test needs more than 1–2 fakes → the code under test is doing too much. Refactor it first.
- If you feel the urge to use
$this->mock(),$this->spy(), or$this->partialMock()on application code → stop. The design is wrong. Fix the behavior into the domain model or use events/listeners instead.
Goal: 100 % green tests with zero mocks of our own code.
This is the only accepted testing style for this project.