# Building a Testing Habit in Laravel: Switching to Pest

> I explain the syntax and structural differences that turned test writing from a chore into a habit when moving from PHPUnit to Pest.

- Published: 2021-09-05
- Category: Tools & Technologies
- Tags: Laravel, Testing
- Reading time: 3 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/building-a-testing-habit-in-laravel-switching-to-pest/
- Language: en-US
- Author: Muhammet Şafak

---
I knew writing tests was a good idea — but for a long time I couldn't close the gap between "I should write tests" and "I'm actually writing tests." [Learning PHPUnit](/en/blog/writing-my-first-tests-with-phpunit/) wasn't hard, but every test file was a class, every test was a method, chains of `$this->assertSomething()` calls… The syntax created a small amount of friction, and as that friction accumulated, test writing kept getting pushed off.

My first reaction to Pest was "another tool?" I waited a while. Early this year I started using Pest on a serious project, and I have no desire to go back to PHPUnit. In this post I'll try to explain why.

## What is Pest

Pest (Pest PHP) is a testing framework built on top of PHPUnit, developed by Nuno Maduro from the Laravel core community. It doesn't replace PHPUnit — it layers a cleaner syntax on top of it. That's an important distinction: your existing PHPUnit tests continue to run with Pest, and the migration can be incremental.

## Syntax Differences

A Feature test with PHPUnit:

```php
<?php

namespace Tests\Feature;

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

class AuthTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_login_with_valid_credentials(): void
    {
        $user = User::factory()->create([
            'password' => bcrypt('gizli123'),
        ]);

        $response = $this->post('/api/login', [
            'email'    => $user->email,
            'password' => 'gizli123',
        ]);

        $response->assertStatus(200)
                 ->assertJsonStructure(['token']);
    }

    public function test_user_cannot_login_with_wrong_password(): void
    {
        $user = User::factory()->create();

        $response = $this->post('/api/login', [
            'email'    => $user->email,
            'password' => 'yanlis',
        ]);

        $response->assertStatus(401);
    }
}
```

The same tests with Pest:

```php
<?php

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

uses(RefreshDatabase::class);

it('kullanıcı geçerli bilgilerle giriş yapabilir', function () {
    $user = User::factory()->create([
        'password' => bcrypt('gizli123'),
    ]);

    $this->post('/api/login', [
        'email'    => $user->email,
        'password' => 'gizli123',
    ])
    ->assertStatus(200)
    ->assertJsonStructure(['token']);
});

it('kullanıcı yanlış şifreyle giriş yapamaz', function () {
    $user = User::factory()->create();

    $this->post('/api/login', [
        'email'    => $user->email,
        'password' => 'yanlis',
    ])
    ->assertStatus(401);
});
```

No class, no `namespace`, no method. Just an `it()` or `test()` function, a string description, and a closure. That's it.

## Expectations Syntax

Pest also ships its own `expect()` API — instead of `assertXxx` methods you can use a fluent chain:

```php
<?php

it('sipariş toplam fiyatı doğru hesaplanır', function () {
    $order = Order::factory()
        ->hasItems(3, ['price' => 100])
        ->create();

    expect($order->total)
        ->toBe(300)
        ->toBeGreaterThan(0);
});
```

Reading `expect()->toBe()` feels far more natural. The test output is equally more readable.

## Parameterized Tests with Datasets

To run the same test with different inputs in PHPUnit you had to write a `dataProvider`. Pest requires much less ceremony:

```php
<?php

it('geçersiz e-posta formatlarını reddeder', function (string $email) {
    $this->post('/api/register', ['email' => $email])
         ->assertStatus(422);
})->with([
    'düz metin',
    '@eksik-lokal.com',
    'eksik-@alan.com',
    'boşluk içeren@alan.com',
]);
```

In PHPUnit you'd define a separate `public function`, add a `@dataProvider` docblock, and match the method name. In Pest it's `.with()` — that's all. This ergonomic improvement encourages testing edge cases; once writing a parameterized test is easy, you naturally cover more scenarios.

## Installation and Migration

```bash
composer require pestphp/pest --dev --with-all-dependencies
composer require pestphp/pest-plugin-laravel --dev
php artisan pest:install
```

Existing PHPUnit tests continue to work untouched. Writing new tests with Pest is enough — incremental migration is fully supported.

One thing to watch during coexistence: helpers registered with `uses()` in `tests/Pest.php` can apply globally to all files. This can sometimes produce unexpected behaviour. If you want to apply `RefreshDatabase` to a specific test group, writing `uses(RefreshDatabase::class)` in that individual file is safer than relying on a global definition.

## Running a Single Test

In a large test suite you sometimes want to run only the test you're currently working on. In Pest you use the `--filter` flag:

```bash
php artisan test --filter "kullanıcı geçerli bilgilerle giriş yapabilir"
```

Or by specifying the test file:

```bash
php artisan test tests/Feature/AuthTest.php
```

Both options exist in PHPUnit too, but because Pest test descriptions are plain strings, `--filter` searching feels more natural. You can write descriptions in any language you like — the filter works the same way.

## The Real Win: Less Friction

What I noticed after adopting Pest wasn't speed (execution time is nearly identical to PHPUnit). The real difference was that the resistance to opening a test file after writing a new feature went down. As the syntax got shorter, the decision to "test this while I'm here" became easier to make.

This is a good example of how tool choice shapes behaviour. Even without a technical difference, the impact on ergonomics is real. You don't have to enjoy writing tests — but you can reasonably want them to generate as little friction as possible.

Switching to Pest made me write tests more often — but the deeper change was in habit. Writing at least the core scenarios before shipping a feature became a reflex. The tool doesn't force that; it just keeps the door open. As friction decreases, so does the urge to say "I'll write that test later."
