Write tests for Laravel (Pest/PHPUnit) and React (Jest/Vitest + React Testing Library). Use when writing unit tests, feature tests, component tests, or integration tests. Covers test structure, assertions, mocking, factories, database testing, and test organization for the Service Provider pattern. Triggers on test, testing, Pest, PHPUnit, Jest, React Testing Library, TDD, or test coverage.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Write tests for Laravel (Pest/PHPUnit) and React (Jest/Vitest + React Testing Library). Use when writing unit tests, feature tests, component tests, or integration tests. Covers test structure, assertions, mocking, factories, database testing, and test organization for the Service Provider pattern. Triggers on test, testing, Pest, PHPUnit, Jest, React Testing Library, TDD, or test coverage.
// tests/Unit/Models/ProjectTest.phpuseApp\Models\Project;
it('scopes to active projects', function () {
Project::factory()->active()->count(2)->create();
Project::factory()->archived()->count(3)->create();
expect(Project::active()->count())->toBe(2);
});
it('returns the formatted deadline', function () {
$project = Project::factory()->make(['deadline' => '2025-06-15']);
expect($project->formatted_deadline)->toBe('Jun 15, 2025');
});
4. Pest Syntax Reference
Basic Structure
// test() styletest('it can create a project', function () {
// arrange, act, assert
});
// it() style (BDD)it('creates a project', function () {
// ...
});
// describe() blocks for groupingdescribe('ProjectService', function () {
describe('create', function () {
it('creates a project from valid data', function () { /* ... */ });
it('throws on invalid data', function () { /* ... */ });
});
describe('delete', function () {
it('soft-deletes the project', function () { /* ... */ });
});
});
beforeEach(function () {
$this->user = User::factory()->create();
$this->service = app(ProjectService::class);
});
afterEach(function () {
Cache::flush();
});
beforeAll(function () {
// Runs once before all tests in the file
});
5. Database Testing
RefreshDatabase Trait
// Pest: globally in Pest.phpuses(Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature');
uses(Illuminate\Foundation\Testing\LazilyRefreshDatabase::class)->in('Feature');
// LazilyRefreshDatabase wraps each test in a transaction (faster than migrating each time)
useIlluminate\Support\Facades\Mail;
useIlluminate\Support\Facades\Event;
useIlluminate\Support\Facades\Notification;
useIlluminate\Support\Facades\Queue;
useIlluminate\Support\Facades\Storage;
it('sends a welcome email on registration', function () {
Mail::fake();
$this->post(route('register'), [
'name' => 'John',
'email' => 'john@example.com',
'password' => 'password',
'password_confirmation' => 'password',
]);
Mail::assertSent(WelcomeMail::class, function ($mail) {
return$mail->hasTo('john@example.com');
});
});
it('dispatches ProjectCreated event', function () {
Event::fake([ProjectCreated::class]);
$user = User::factory()->create();
$this->actingAs($user)->post(route('projects.store'), [
'title' => 'Test',
'description' => 'Test desc',
]);
Event::assertDispatched(ProjectCreated::class, function ($event) {
return$event->project->title === 'Test';
});
});
it('queues an export job', function () {
Queue::fake();
$user = User::factory()->create();
$this->actingAs($user)->post(route('projects.export'));
Queue::assertPushed(ExportProjectsJob::class);
});
it('stores an uploaded file', function () {
Storage::fake('s3');
$file = UploadedFile::fake()->image('avatar.jpg');
$this->actingAs(User::factory()->create())
->post(route('profile.avatar'), ['avatar' => $file]);
Storage::disk('s3')->assertExists('avatars/' . $file->hashName());
});
Mocking Services in the Container
useApp\Services\PaymentGateway;
useMockery;
it('processes payment through gateway', function () {
$mock = Mockery::mock(PaymentGateway::class);
$mock->shouldReceive('charge')
->once()
->with(1000, 'usd')
->andReturn(true);
$this->app->instance(PaymentGateway::class, $mock);
$this->actingAs(User::factory()->create())
->post(route('payments.store'), ['amount' => 1000]);
});
// Using Pest's mock() helperit('calls external API', function () {
$this->mock(ExternalApiService::class)
->shouldReceive('fetch')
->once()
->andReturn(['data' => 'value']);
// test code that uses ExternalApiService
});
Partial Mocks and Spies
// Spy: verify after the fact (no expectations upfront)it('logs the activity', function () {
$spy = $this->spy(ActivityLogger::class);
$this->actingAs(User::factory()->create())
->post(route('projects.store'), ['title' => 'Test', 'description' => 'Desc']);
$spy->shouldHaveReceived('log')
->with('project.created', Mockery::type(Project::class));
});
// Partial mock: only mock specific methodsit('uses real methods except external call', function () {
$this->partialMock(ProjectService::class, function ($mock) {
$mock->shouldReceive('notifySlack')->andReturn(true);
});
// Other methods on ProjectService remain real
});
PHP test files: {Action}{Model}Test.php (e.g., CreateProjectTest.php)
React test files: {ComponentName}.test.tsx
Test descriptions: start with it + present-tense verb (e.g., it('creates a project'))
Group related tests with describe() blocks
One assertion concept per test (a test can have multiple expect() calls if they assert the same concept)
Running Tests
# PHP tests
php artisan test# all tests
php artisan test --filter=ProjectTest # filter by name
php artisan test --parallel # parallel execution
php artisan test --coverage # with coverage (requires Xdebug/PCOV)
./vendor/bin/pest --dirty # only test files changed since last commit# JavaScript tests
npx vitest # watch mode
npx vitest run # single run
npx vitest run --coverage # with coverage
npx vitest run tests/js/Components # specific directory