메인 콘텐츠로 건너뛰기

왜 Pest가 Laravel의 기본이 되었는가

Laravel 11부터 laravel new로 프로젝트를 만들 때 Pest가 기본 테스트 프레임워크로 선택되게 되었습니다. PHPUnit은 오랜 세월 PHP의 표준 테스트 도구로서 사용되어 왔지만, Pest는 그 PHPUnit을 베이스로 하면서, 보다 간결하고 읽기 쉬운 테스트 표기법을 제공합니다.
Pest는 PHPUnit 상에서 동작하므로, 기존의 PHPUnit 테스트는 그대로 실행할 수 있습니다. 이관은 단계적으로 진행할 수 있습니다.
공식이 Pest를 채용한 배경에는 테스트를 작성할 때의 마찰을 줄인다는 목표가 있습니다. 클래스를 정의하고 메서드를 쓰는 의식적인 절차를 없애고, “테스트하고 싶은 것” 그 자체에 집중할 수 있는 표기법을 제공함으로써, 테스트를 쓰는 습관이 자연스럽게 몸에 배게 됩니다.

PHPUnit과의 주요 차이

테스트의 표기법

가장 알기 쉬운 차이는 테스트의 작성법입니다.
test('사용자가 로그인할 수 있다', function () {
    $user = User::factory()->create();

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

    $response->assertRedirect('/dashboard');
});
Pest의 test() 함수는 클로저를 받습니다. 클래스나 메서드의 정의가 불필요하므로, 테스트의 의도가 첫 줄부터 명확합니다. it()도 마찬가지로 사용할 수 있습니다. 영어로 쓰는 경우는 it('can login', ...)이라는 자연스러운 문장으로 읽을 수 있습니다.

expect() API

Pest의 최대 특징은 expect()를 사용한 체인 형식의 어서션입니다.
test('게시물 목록 API가 JSON 형식으로 반환된다', function () {
    $posts = Post::factory(3)->create();

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

    expect($response->status())->toBe(200);
    expect($response->json('data'))->toHaveCount(3);
    expect($response->json('data.0.title'))->not->toBeEmpty();
});
expect($value)->toBe(), ->toBeNull(), ->toContain(), ->toHaveCount() 등, 영어로서 자연스럽게 읽을 수 있는 형식으로 어서션을 작성할 수 있습니다. 여러 어서션을 메서드 체인으로 정리할 수도 있습니다.
expect($user)
    ->name->toBe('다나카 타로')
    ->email->toBe('[email protected]')
    ->is_admin->toBeFalse();

셋업과 티어다운

PHPUnit의 setUp()에 해당하는 것이 beforeEach(), tearDown()에 해당하는 것이 afterEach()입니다.
beforeEach(function () {
    $this->user = User::factory()->create();
    $this->actingAs($this->user);
});

test('인증된 사용자는 프로필에 접근할 수 있다', function () {
    $response = $this->get('/profile');
    $response->assertOk();
});

test('인증된 사용자는 프로필을 갱신할 수 있다', function () {
    $response = $this->patch('/profile', ['name' => '새로운 이름']);
    $response->assertRedirect('/profile');
});

데이터셋 — 테이블 드리븐 테스트

같은 테스트 로직을 여러 데이터로 실행하고 싶은 경우는 dataset을 사용합니다.
test('무효한 메일 주소는 유효성 검증 오류가 된다', function (string $email) {
    $response = $this->postJson('/api/users', [
        'name' => '다나카 타로',
        'email' => $email,
    ]);

    $response->assertUnprocessable();
})->with([
    '메일 없음' => [''],
    '형식 부정' => ['notanemail'],
    '이중 @' => ['a@@example.com'],
]);
각 데이터셋의 라벨('메일 없음' 등)이 테스트 이름에 부가되므로, 어느 패턴에서 실패했는지 한눈에 알 수 있습니다.

arch() 테스트 — 아키텍처 자동 체크

Pest의 arch()는 코드베이스의 구조를 검증하는 테스트입니다. “컨트롤러가 모델에 직접 의존하지 않는지”, “모델이 Eloquent를 상속하고 있는지”와 같은 아키텍처 룰을 자동으로 검증할 수 있습니다.
test('모델은 Eloquent를 상속한다', function () {
    arch()->expect('App\Models')
        ->toExtend(Illuminate\Database\Eloquent\Model::class);
});

test('컨트롤러는 final로 정의된다', function () {
    arch()->expect('App\Http\Controllers')
        ->toBeFinal();
});

test('서비스 클래스는 컨트롤러에 의존하지 않는다', function () {
    arch()->expect('App\Services')
        ->not->toUse('App\Http\Controllers');
});
arch() 테스트는 소스 코드를 정적 해석하여 실행됩니다. 실제로 HTTP 요청을 보내거나 데이터베이스에 접근하지 않으므로 매우 고속입니다.
Pest에는 자주 사용되는 아키텍처 룰의 프리셋도 준비되어 있습니다.
test('Laravel의 아키텍처 룰을 따르고 있다', function () {
    arch()->preset()->laravel();
});
laravel() 프리셋은 모델의 명명 · 컨트롤러의 상속 · 미들웨어의 구조 등, Laravel의 일반적인 관습에 따른 룰을 일괄 검증합니다.

Laravel 프로젝트에서의 실전 예

테스트 작성

php artisan make:test UserTest
Laravel 11 이후 기본 설정 그대로 실행하면, Pest 형식의 테스트 파일이 생성됩니다.
<?php

test('example', function () {
    expect(true)->toBeTrue();
});

Laravel의 테스트 헬퍼를 그대로 사용

Pest는 Laravel의 TestCase를 상속하고 있으므로, actingAs(), assertDatabaseHas(), HTTP 테스트 헬퍼 등 모든 Laravel 테스트 헬퍼를 그대로 사용할 수 있습니다.
use App\Models\Post;
use App\Models\User;

test('사용자는 자신의 게시물을 삭제할 수 있다', function () {
    $user = User::factory()->create();
    $post = Post::factory()->for($user)->create();

    $response = $this
        ->actingAs($user)
        ->delete("/posts/{$post->id}");

    $response->assertRedirect('/posts');
    $this->assertModelMissing($post);
});

test('다른 사용자의 게시물은 삭제할 수 없다', function () {
    $user = User::factory()->create();
    $otherUser = User::factory()->create();
    $post = Post::factory()->for($otherUser)->create();

    $this
        ->actingAs($user)
        ->delete("/posts/{$post->id}")
        ->assertForbidden();

    $this->assertModelExists($post);
});

팩토리와 데이터베이스 트랜잭션

RefreshDatabaseDatabaseTransactions 트레이트도 uses()로 간단히 적용할 수 있습니다. 파일 첫머리에 쓰면 그 파일 전체에 적용됩니다.
uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature');
tests/Pest.php에 한 번 설정해 두면, 모든 Feature 테스트에서 RefreshDatabase가 자동으로 유효해집니다. 개별 테스트 파일에서 덮어쓰는 것도 가능합니다.

기존 PHPUnit 테스트와의 공존 방법

Pest와 PHPUnit은 동일한 프로젝트 내에서 공존할 수 있습니다. 기존 PHPUnit 테스트를 다시 작성할 필요는 없고, 새 테스트부터 Pest 형식으로 쓰기 시작하면 됩니다.
./vendor/bin/pest       # Pest로 전 테스트 실행 (PHPUnit 테스트도 포함)
./vendor/bin/pest --filter="사용자"  # 테스트 이름으로 필터링
php artisan test        # Artisan 명령으로도 Pest가 동작
php artisan test는 Laravel 11 이후, Pest가 설치되어 있는 경우 자동으로 Pest를 사용하여 실행합니다.

이관 진행 방법

  1. 우선 tests/Pest.phpuses()의 설정을 추가한다
  2. 새로 추가하는 테스트는 Pest 형식으로 쓴다
  3. 기존 PHPUnit 테스트는 동작을 확인하면서 조금씩 이관한다
서둘러 전 테스트를 다시 쓸 필요는 없습니다. Pest의 문법은 PHPUnit에 비해 학습 비용이 낮으므로, 팀으로의 침투도 비교적 부드럽습니다.

정리

비교 항목PHPUnitPest
테스트 정의클래스 + 메서드test() / it() 함수
어서션$this->assert*()expect()->to*()
셋업setUp()beforeEach()
데이터 구동@dataProvider->with()
아키텍처 검증없음arch()
실행 속도표준동등 (PHPUnit 상에서 동작)
Pest는 PHPUnit의 치환이 아니라, PHPUnit을 랩한 상위 레이어입니다. Laravel과의 통합도 깊고, 스타터 킷 생성 시부터 기본으로 선택되는 정도가 되어 있습니다. 기존 프로젝트로의 도입 비용은 낮고, 새 테스트부터 시험 삼아 시작하는 것만으로 혜택을 받을 수 있습니다. 테스트를 쓰는 양이 늘면 버그의 발견이 빨라지고, 리팩토링에 대한 심리적 허들도 낮아집니다. Pest는 그 “테스트를 쓰는 것 자체의 마찰”을 줄이기 위한 도구입니다.

Pest 공식 문서

데이터셋, 커버리지, 병렬 실행 등 Pest의 모든 기능에 대해서는 공식 문서를 참고하세요.
마지막 수정일 2026년 7월 13일