Have you ever run a Laravel test suite and waited more than 10 seconds for a result? Or worse: watched a test fail without understanding why, spending an hour chasing a wrong mock? It happens to everyone. The problem is many developers treat tests as a boring obligation, not a tool to write better code faster.
At Meteora Web, we have been working with Laravel on real projects for years: custom platforms, e-commerce, APIs for clients. We experienced the radical shift that Pest PHP brings. It's not just prettier syntax: it's a paradigm change in readability and writing speed. In this guide we show you how to move from heavy PHPUnit tests to lean Pest tests, and especially how to decide when to use a unit test versus a feature test so you don't waste time.
Why is Pest PHP better than classic PHPUnit for Laravel tests?
PHPUnit is the de facto standard, but its classes and methods inherited from TestCase make tests verbose. Every test requires a class, a method, and often lengthy setup. Pest rewrites the experience: it gives you nested functions, natural language descriptions, and clarity that makes a difference when you come back to code months later.
Concrete example: same test in PHPUnit and Pest.
Sponsored Protocol
// PHPUnit class
class UserTest extends TestCase
{
public function test_it_creates_a_user()
{
$user = User::factory()->create();
$this->assertDatabaseHas('users', ['email' => $user->email]);
}
}
// Pest
test('creates a user', function () {
$user = User::factory()->create();
$this->assertDatabaseHas('users', ['email' => $user->email]);
});
Pest is more compact, does not require classes, and supports higher order tests to chain assertions fluidly. Functions beforeEach() and afterEach() replace setUp() and tearDown() with a declarative syntax.
How to distinguish a Unit Test from a Feature Test in Laravel?
This is the question that causes most damage. Choosing the wrong level means either writing fragile tests (that break on the first refactoring) or too slow tests (that call the database when not needed).
Unit tests: isolated and fast
A unit test verifies a single class or method, without touching the database, routes, or other external dependencies. It uses mocks or stubs to isolate the subject. When to use: for services, helpers, calculations, pure business logic. In Laravel, an example is testing a SubscriptionService that calculates expiry date.
Sponsored Protocol
// Pest - unit test with mock
test('subscription is active when end date is in future', function () {
$service = new SubscriptionService();
$user = Mockery::mock(User::class);
$user->shouldReceive('subscriptionEndAt')
->once()
->andReturn(now()->addDays(10));
expect($service->isActive($user))->toBeTrue();
});Speed: these tests run in milliseconds, no database connection. Run them hundreds of times during development.
Feature tests: lightweight integration
A feature test in Laravel checks a complete flow: an HTTP request, controller logic, database interaction, and response. It does not mock Eloquent or the database; it uses database transactions (via RefreshDatabase or DatabaseTransactions). They are slower but cover the value chain.
// Pest - feature test for API
test('guests can register', function () {
$response = $this->postJson('/api/register', [
'name' => 'Mario Rossi',
'email' => 'mario@example.com',
'password' => 'Password123!',
'password_confirmation' => 'Password123!',
]);
$response->assertStatus(201)
->assertJsonStructure(['data' => ['token', 'user']]);
$this->assertDatabaseHas('users', ['email' => 'mario@example.com']);
});Rule of thumb: if the test touches the database or routes, it's a feature test. If it isolates a single class, it's a unit test. Never mix the two in the same suite to avoid confusing execution times.
Sponsored Protocol
How to structure Pest tests in a Laravel project?
Install Pest with composer require pestphp/pest --dev and configure via tests/Pest.php. At Meteora Web we use this structure:
tests/
├── Feature/
│ ├── Auth/
│ │ ├── LoginTest.php
│ │ └── RegisterTest.php
│ └── Api/
│ └── ProductsTest.php
├── Unit/
│ ├── Services/
│ │ └── SubscriptionServiceTest.php
│ └── Helpers/
│ └── PriceFormatterTest.php
├── Pest.php
└── TestCase.phpEach Test.php file contains multiple tests using test() or describe() to group related tests. Use describe to organise contexts:
describe('SubscriptionService', function () {
test('returns true for active subscriptions', function () { ... });
test('returns false for expired subscriptions', function () { ... });
});Pest automatically generates descriptive labels in reports, making tests much more readable.
Sponsored Protocol
What best practices ensure fast and reliable tests?
We have seen too many projects with tests that take 3 minutes to run. Here are our rules:
- Use RefreshDatabase wisely: for feature tests, use
RefreshDatabaseonly when necessary; oftenDatabaseTransactionsis faster because it does not run migrations every test. - Don't mock what you don't own: Avoid mocking external library classes unless they are truly slow. Use their real methods when possible.
- Factories with states: Define specific states in factories (e.g.,
User::factory()->unverified()->create()) to create precise data without duplicating code in tests. - Test only what must work: Don't cover every setter with a test. Focus on critical behaviors: validation, calculations, authorization, edge cases.
- Use
WithFakersparingly: Prefer deterministic data to avoid flaky tests.
How to run tests in CI and integrate with your workflow?
We run tests on every push with GitHub Actions. Pest integrates perfectly with php artisan test (uses Pest by default if installed). A minimal workflow example:
name: Laravel Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction
- run: cp .env.example .env
- run: php artisan key:generate
- run: php artisan migrate --force
- run: php artisan testWith Pest, error reports are clear and include the describe context, making debugging easier.
Sponsored Protocol
What to do now
- Install Pest in your Laravel project:
composer require pestphp/pest --devand follow the official migration guide. - Convert an existing bugfix into a test: before fixing a bug, write a test that reproduces it. Then fix. This is the most pragmatic TDD cycle.
- Separate unit tests from feature tests: move tests that don't touch the database to
tests/Unitand integration tests totests/Feature. - Define three states for each factory: default, unverified, admin. They cover 80% of cases.
- Read the official Pest documentation to dive deeper into higher order tests and advanced test architectures.
If you want to understand how testing fits into a full Laravel enterprise architecture, visit our Laravel pillar page.