> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Pest를 이용한 고급 테스트

> Laravel 11 이후에 기본이 된 Pest의 고급 사용법을, Expectation API·데이터셋·페이크·Mockery까지 체계적으로 해설합니다.

## Pest란

[Pest](https://pestphp.com)는 PHPUnit 위에 구축된 테스트 프레임워크로, Laravel 11 이후의 신규 프로젝트에 기본으로 채용되어 있습니다. PHPUnit의 풍부한 어서션군을 그대로 이용하면서, 클로저 기반의 간결한 구문으로 테스트를 기술할 수 있습니다.

PHPUnit과의 주요 차이:

| 관점     | Pest                                 | PHPUnit                  |
| ------ | ------------------------------------ | ------------------------ |
| 테스트 정의 | `test()` / `it()` 클로저                | 메서드로 정의하는 클래스            |
| 어서션    | Expectation API (`expect()->toBe()`) | `$this->assert*()`       |
| 데이터셋   | `dataset()` / `with()`               | `@dataProvider`          |
| 훅      | `beforeEach()` / `afterEach()`       | `setUp()` / `tearDown()` |
| 네스트    | `describe()`로 그룹화                    | 클래스로 분리                  |

<Info>
  Pest의 테스트는 PHPUnit으로 실행되므로, 기존의 PHPUnit 테스트와 공존할 수 있습니다. `php artisan test` 또는 `vendor/bin/pest`로 실행합니다.
</Info>

## `describe` / `it` / `test`의 사용 구분

### `test()`

가장 심플한 정의. 테스트명을 그대로 설명문으로서 사용합니다.

```php theme={null}
test('ユーザーはメールアドレスでログインできる', function () {
    $user = User::factory()->create();

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

    $response->assertRedirect('/dashboard');
});
```

### `it()`

"it should..."와 같은 영어적인 문체로 자연 언어에 가까운 기술을 할 수 있습니다.

```php theme={null}
it('認証されていないユーザーをログインページにリダイレクトする', function () {
    $response = $this->get('/dashboard');

    $response->assertRedirect('/login');
});
```

### `describe()`

관련된 테스트를 그룹화합니다. `beforeEach()`의 공유나 파일 내에서의 논리적인 정리에 사용합니다.

```php theme={null}
describe('注文管理', function () {
    beforeEach(function () {
        $this->user = User::factory()->create();
        $this->actingAs($this->user);
    });

    it('注文一覧を取得できる', function () {
        Order::factory(3)->for($this->user)->create();

        $response = $this->get('/orders');

        $response->assertOk()->assertJsonCount(3, 'data');
    });

    it('他のユーザーの注文は取得できない', function () {
        $other = User::factory()->create();
        Order::factory()->for($other)->create();

        $response = $this->get('/orders');

        $response->assertOk()->assertJsonCount(0, 'data');
    });
});
```

<Tip>
  `describe()`는 네스트할 수 있습니다. 다만 지나치게 깊은 네스트는 테스트를 읽기 어렵게 하므로, 2계층까지를 기준으로 합시다.
</Tip>

## Expectation API

`expect()`는 Pest 독자의 어서션 구문입니다. 메서드 체인으로 조건을 쌓아 올릴 수 있습니다.

### 기본적인 어서션

```php theme={null}
expect($value)->toBe(42);               // 厳密な等価（===）
expect($value)->toEqual(['a' => 1]);    // 緩やかな等価（==）
expect($value)->toBeTrue();
expect($value)->toBeFalse();
expect($value)->toBeNull();
expect($value)->not->toBeNull();

expect($string)->toContain('Laravel');
expect($array)->toHaveCount(3);
expect($array)->toHaveKey('email');
expect($array)->toContain('admin');

expect($number)->toBeGreaterThan(0);
expect($number)->toBeLessThanOrEqual(100);
```

### 모델에 대한 어서션

```php theme={null}
expect($user)->toBeInstanceOf(User::class);
expect($user->email)->toMatchRegex('/^.+@.+\..+$/');

// データベースへの保存を確認
expect(User::where('email', 'test@example.com')->exists())->toBeTrue();
```

### `and()` 체인

```php theme={null}
expect($response->status())->toBe(200)
    ->and($response->json('name'))->toBe('Laravel')
    ->and($response->json('version'))->toBeGreaterThan(12);
```

### `each()`로 배열의 각 요소를 검증

```php theme={null}
$users = User::factory(3)->create();

expect($users)->each(function ($user) {
    $user->toBeInstanceOf(User::class)
        ->email->not->toBeNull();
});
```

## 데이터셋을 사용한 파라미터화 테스트

같은 로직을 여러 입력값으로 테스트하려면 `dataset()` 또는 인라인 `with()`를 사용합니다.

### 인라인 데이터셋

```php theme={null}
it('無効なメールアドレスはバリデーションエラーになる', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with([
    'プレーンテキスト' => ['not-an-email'],
    'ドメインなし' => ['user@'],
    '空文字' => [''],
]);
```

### 이름 붙은 데이터셋

`tests/Datasets` 디렉토리에 데이터셋을 정의합니다.

```php theme={null}
// tests/Datasets/InvalidEmails.php
dataset('invalid_emails', [
    'plain' => ['not-an-email'],
    'no-domain' => ['user@'],
    'empty' => [''],
    'spaces' => ['user @example.com'],
]);
```

```php theme={null}
it('無効なメールアドレスはバリデーションエラーになる', function (string $email) {
    $response = $this->post('/register', ['email' => $email]);

    $response->assertInvalid('email');
})->with('invalid_emails');
```

### 클로저를 사용한 동적 데이터셋

```php theme={null}
it('ユーザープランごとに異なる制限がある', function (string $plan, int $limit) {
    $user = User::factory()->create(['plan' => $plan]);

    expect($user->requestLimit())->toBe($limit);
})->with([
    ['free', 100],
    ['pro', 1000],
    ['enterprise', 10000],
]);
```

## Mockery를 사용한 유닛 테스트

### 서비스의 모크

```php theme={null}
use App\Services\PaymentGateway;
use Mockery\MockInterface;

test('決済サービスが呼び出される', function () {
    $mock = $this->mock(PaymentGateway::class, function (MockInterface $mock) {
        $mock->expects('charge')
            ->with(1000, 'jpy')
            ->andReturn(['status' => 'succeeded']);
    });

    $result = app(PaymentGateway::class)->charge(1000, 'jpy');

    expect($result['status'])->toBe('succeeded');
});
```

### 스파이

스파이는 모크와 달리, 구현을 호출하면서 호출 기록을 남깁니다.

```php theme={null}
use App\Services\NotificationService;

test('通知サービスが呼び出されたか確認する', function () {
    $spy = $this->spy(NotificationService::class);

    $this->post('/orders', ['amount' => 1000]);

    $spy->shouldHaveReceived('send')->once()->with('order.created');
});
```

### 부분 모크

일부 메서드만 모크하고, 다른 것은 실제 구현을 사용합니다.

```php theme={null}
use App\Services\ReportService;
use Mockery\MockInterface;

test('レポートのファイル書き込みをスキップする', function () {
    $mock = $this->partialMock(ReportService::class, function (MockInterface $mock) {
        $mock->expects('writeFile')->andReturnNull();
    });

    $result = $mock->generate();

    expect($result)->not->toBeNull();
});
```

## HTTP 페이크에 의한 외부 API 호출의 테스트

`Http::fake()`를 사용하면 실제 HTTP 요청을 보내지 않고 응답을 스텁할 수 있습니다.

### 기본적인 페이크

```php theme={null}
use Illuminate\Support\Facades\Http;

test('外部APIからユーザー情報を取得する', function () {
    Http::fake([
        'https://api.example.com/users/*' => Http::response([
            'id' => 1,
            'name' => 'Laravel User',
        ], 200),
    ]);

    $result = app(UserApiClient::class)->find(1);

    expect($result['name'])->toBe('Laravel User');
    Http::assertSent(fn ($request) => $request->url() === 'https://api.example.com/users/1');
});
```

### 에러 응답의 테스트

```php theme={null}
test('APIがエラーを返したときに例外が発生する', function () {
    Http::fake([
        'api.example.com/*' => Http::response([], 503),
    ]);

    expect(fn () => app(UserApiClient::class)->find(1))
        ->toThrow(\App\Exceptions\ApiUnavailableException::class);
});
```

### 네트워크 장애의 시뮬레이션

```php theme={null}
use Illuminate\Http\Client\ConnectionException;

test('接続エラーをハンドリングする', function () {
    Http::fake(fn () => throw new ConnectionException('Connection refused'));

    $result = app(UserApiClient::class)->findWithFallback(1);

    expect($result)->toBeNull();
});
```

## 이벤트·메일·알림의 페이크

### `Event::fake()`

```php theme={null}
use Illuminate\Support\Facades\Event;
use App\Events\OrderCreated;

test('注文作成時にイベントが発行される', function () {
    Event::fake();

    $this->post('/orders', ['product_id' => 1, 'amount' => 1000]);

    Event::assertDispatched(OrderCreated::class, function ($event) {
        return $event->order->amount === 1000;
    });
});
```

특정 이벤트만 페이크하고, 다른 것은 실제로 처리시킬 수도 있습니다.

```php theme={null}
Event::fake([OrderCreated::class]);
```

### `Mail::fake()`

```php theme={null}
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;

test('ユーザー登録時にウェルカムメールが送信される', function () {
    Mail::fake();

    $this->post('/register', [
        'name' => 'Test User',
        'email' => 'test@example.com',
        'password' => 'password',
        'password_confirmation' => 'password',
    ]);

    Mail::assertSent(WelcomeEmail::class, function ($mail) {
        return $mail->hasTo('test@example.com');
    });
});
```

### `Notification::fake()`

```php theme={null}
use Illuminate\Support\Facades\Notification;
use App\Notifications\OrderShipped;

test('出荷時に通知が送られる', function () {
    Notification::fake();

    $user = User::factory()->create();
    $order = Order::factory()->for($user)->create();

    $this->post("/orders/{$order->id}/ship");

    Notification::assertSentTo($user, OrderShipped::class, function ($notification) use ($order) {
        return $notification->order->id === $order->id;
    });
});
```

## Artisan 커맨드의 테스트

```php theme={null}
use Illuminate\Support\Facades\Artisan;

test('不要なデータを削除するコマンドが動作する', function () {
    $expired = Order::factory()->create(['expires_at' => now()->subDay()]);
    $valid = Order::factory()->create(['expires_at' => now()->addDay()]);

    $this->artisan('orders:cleanup')
        ->assertSuccessful()
        ->expectsOutput('Cleaned up 1 expired order(s).');

    expect(Order::find($expired->id))->toBeNull();
    expect(Order::find($valid->id))->not->toBeNull();
});
```

### 대화형 커맨드의 테스트

```php theme={null}
test('対話型コマンドで確認後に処理が実行される', function () {
    $this->artisan('reports:generate')
        ->expectsQuestion('実行しますか？', 'yes')
        ->expectsOutput('レポートを生成しました。')
        ->assertExitCode(0);
});
```

## `RefreshDatabase`와 `LazilyRefreshDatabase`

### `RefreshDatabase`

각 테스트 후에 데이터베이스를 롤백하고, 클린한 상태를 유지합니다. 테스트 스위트 개시 시에 마이그레이션을 실행합니다.

```php theme={null}
// tests/Pest.php
uses(RefreshDatabase::class)->in('Feature');
```

```php theme={null}
use Illuminate\Foundation\Testing\RefreshDatabase;

test('ユーザーを作成できる', function () {
    $user = User::factory()->create(['name' => 'Laravel']);

    expect(User::count())->toBe(1);
    expect($user->name)->toBe('Laravel');
});
```

### `LazilyRefreshDatabase`

`RefreshDatabase`는 모든 테스트에서 마이그레이션을 확인하지만, `LazilyRefreshDatabase`는 데이터베이스를 실제로 변경하는 테스트가 실행될 때까지 마이그레이션을 지연시킵니다. 데이터베이스를 만지지 않는 테스트가 많은 경우에 고속화할 수 있습니다.

```php theme={null}
// tests/Pest.php
uses(LazilyRefreshDatabase::class)->in('Feature');
```

<Tip>
  대부분의 프로젝트에서는 `RefreshDatabase`가 안전한 선택지입니다. `LazilyRefreshDatabase`는 테스트 스위트가 커지고 데이터베이스 조작이 없는 테스트가 많은 경우에 검토합시다.
</Tip>

| 관점                   | `RefreshDatabase` | `LazilyRefreshDatabase`          |
| -------------------- | ----------------- | -------------------------------- |
| 마이그레이션 실행 타이밍        | 테스트 스위트 개시 시      | 최초의 DB 조작 시                      |
| DB를 만지지 않는 테스트의 오버헤드 | 있음                | 없음                               |
| 권장 장면                | 일반적인 케이스          | 테스트 수가 많고 DB를 사용하지 않는 테스트가 많은 경우 |

## 코드 커버리지의 계측

Xdebug 또는 PCOV가 필요합니다.

```bash theme={null}
# カバレッジをターミナルに表示
php artisan test --coverage

# 最低カバレッジを強制（下回るとCIが失敗）
php artisan test --coverage --min=80

# HTMLレポートを出力
vendor/bin/pest --coverage-html=coverage/

# 低速なテストを確認
php artisan test --profile
```

<Warning>
  코드 커버리지의 계측은 테스트 실행 시간을 대폭 증가시킵니다. CI 환경에서는 전용 잡으로 분리하거나, 풀 리퀘스트 시에만 실행하도록 설정하는 것을 추천합니다.
</Warning>

## 관련 페이지

<Card title="테스트 입문" icon="flask" href="/ko/testing">
  Laravel에서의 테스트의 기본적인 작성법과 `php artisan test`의 사용법을 확인합니다.
</Card>


## Related topics

- [패키지의 버전 호환성 관리](/ko/advanced/package-versioning.md)
- [테스트](/ko/packages/laravel-bluesky/testing.md)
- [Pest PHP로 시작하는 Laravel 테스트](/ko/blog/pest-introduction.md)
- [테스트 입문](/ko/testing.md)
- [Eloquent Factories](/ko/eloquent-factories.md)
