| name | laravel-tdd |
| description | PHPUnit과 Pest, 팩토리, 데이터베이스 테스트, 페이크(fakes) 및 커버리지 목표를 사용한 라라벨 TDD(테스트 주도 개발) 가이드입니다. |
| origin | ECC |
라라벨 TDD 워크플로우 (Laravel TDD Workflow)
PHPUnit과 Pest를 사용하여 80% 이상의 커버리지(단위 + 기능)를 목표로 하는 라라벨 애플리케이션용 테스트 주도 개발(TDD) 가이드입니다.
사용 시점
- 라라벨에서 새로운 기능이나 엔드포인트를 구현할 때
- 버그 수정 또는 리팩터링 시
- Eloquent 모델, 정책(Policies), 잡(Jobs) 및 알림(Notifications)을 테스트할 때
- 프로젝트가 이미 PHPUnit으로 표준화되어 있지 않다면 새로운 테스트에는 Pest 사용을 권장합니다.
동작 방식
Red-Green-Refactor 주기
- 실패하는 테스트 작성
- 테스트를 통과시키기 위한 최소한의 코드 구현
- 테스트 통과를 유지하면서 리팩터링 수행
테스트 레이어
- 단위(Unit): 순수 PHP 클래스, 값 객체(Value Objects), 서비스
- 기능(Feature): HTTP 엔드포인트, 인증, 유효성 검사, 정책
- 통합(Integration): 데이터베이스 + 큐 + 외부 경계 시스템
범위에 따라 레이어를 선택하세요:
- 순수 비즈니스 로직과 서비스에는 단위 테스트를 사용하세요.
- HTTP, 인증, 유효성 검사 및 응답 구조 확인에는 기능 테스트를 사용하세요.
- DB/큐/외부 서비스를 함께 검증할 때는 통합 테스트를 사용하세요.
데이터베이스 전략
- 대부분의 기능/통합 테스트에는
RefreshDatabase를 사용하세요. (테스트 실행 시 한 번 마이그레이션을 수행하고, 지원되는 경우 각 테스트를 트랜잭션으로 감쌉니다. 인메모리 DB의 경우 테스트마다 재마이그레이션될 수 있습니다.)
- 스키마가 이미 마이그레이션되어 있고 테스트별 롤백만 필요한 경우에는
DatabaseTransactions를 사용하세요.
- 매 테스트마다 전체 마이그레이션/초기화가 필요하고 그 비용을 감수할 수 있는 경우에만
DatabaseMigrations를 사용하세요.
데이터베이스를 사용하는 테스트의 기본값으로 RefreshDatabase를 사용하세요. 트랜잭션을 지원하는 DB의 경우 테스트 실행당 한 번 마이그레이션을 수행하고 각 테스트를 트랜잭션으로 래핑합니다. :memory: SQLite나 트랜잭션을 지원하지 않는 연결의 경우 매 테스트 전에 마이그레이션합니다.
테스트 프레임워크 선택
- 가능하면 새로운 테스트에는 Pest를 기본으로 사용하세요.
- 프로젝트가 이미 PHPUnit으로 표준화되어 있거나 PHPUnit 전용 도구가 필요한 경우에만 PHPUnit을 사용하세요.
예시
PHPUnit 예시
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectControllerTest extends TestCase
{
use RefreshDatabase;
public function test_owner_can_create_project(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/projects', [
'name' => 'New Project',
]);
$response->assertCreated();
$this->assertDatabaseHas('projects', ['name' => 'New Project']);
}
}
기능 테스트 예시 (HTTP 레이어)
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectIndexTest extends TestCase
{
use RefreshDatabase;
public function test_projects_index_returns_paginated_results(): void
{
$user = User::factory()->create();
Project::factory()->count(3)->for($user)->create();
$response = $this->actingAs($user)->getJson('/api/projects');
$response->assertOk();
$response->assertJsonStructure(['success', 'data', , ]);
}
}
Pest 예시
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseHas;
uses(RefreshDatabase::class);
test('owner can create project', function () {
$user = User::factory()->create();
$response = actingAs($user)->postJson('/api/projects', [
'name' => 'New Project',
]);
$response->assertCreated();
assertDatabaseHas('projects', ['name' => 'New Project']);
});
Pest 기능 테스트 예시 (HTTP 레이어)
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
uses(RefreshDatabase::class);
test('projects index returns paginated results', function () {
$user = User::factory()->create();
Project::factory()->count(3)->for($user)->create();
$response = actingAs($user)->getJson('/api/projects');
$response->assertOk();
$response->assertJsonStructure(['success', 'data', 'error', 'meta']);
});
팩토리 및 상태 (Factories and States)
- 테스트 데이터 생성에 팩토리를 사용하세요.
- 예외 케이스(보관됨, 관리자, 체험판 등)를 위해 상태(states)를 정의하세요.
$user = User::factory()->state(['role' => 'admin'])->create();
데이터베이스 테스트
- 깨끗한 상태를 위해
RefreshDatabase를 사용하세요.
- 테스트를 격리하고 결정적으로 유지하세요.
- 수동 쿼리보다는
assertDatabaseHas 사용을 권장합니다.
영속성 테스트 예시
use App\Models\Project;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class ProjectRepositoryTest extends TestCase
{
use RefreshDatabase;
public function test_project_can_be_retrieved_by_slug(): void
{
$project = Project::factory()->create(['slug' => 'alpha']);
$found = Project::query()->where('slug', 'alpha')->firstOrFail();
$this->assertSame($project->id, $found->id);
}
}
부수 효과를 위한 페이크 (Fakes)
- 잡(Jobs)에는
Bus::fake()
- 큐 작업에는
Queue::fake()
- 알림에는
Mail::fake() 및 Notification::fake()
- 도메인 이벤트에는
Event::fake()
use Illuminate\Support\Facades\Queue;
Queue::fake();
dispatch(new SendOrderConfirmation($order->id));
Queue::assertPushed(SendOrderConfirmation::class);
use Illuminate\Support\Facades\Notification;
Notification::fake();
$user->notify(new InvoiceReady($invoice));
Notification::assertSentTo($user, InvoiceReady::class);
인증 테스트 (Sanctum)
use Laravel\Sanctum\Sanctum;
Sanctum::actingAs($user);
$response = $this->getJson('/api/projects');
$response->assertOk();
HTTP 및 외부 서비스
- 외부 API를 격리하기 위해
Http::fake()를 사용하세요.
Http::assertSent()로 외부로 전송된 페이로드를 검증하세요.
커버리지 목표
- 단위 + 기능 테스트를 합쳐 80% 이상의 커버리지를 준수하세요.
- CI 환경에서
pcov 또는 XDEBUG_MODE=coverage를 사용하세요.
테스트 명령어
php artisan test
vendor/bin/phpunit
vendor/bin/pest
테스트 설정
- 빠른 테스트를 위해
phpunit.xml에서 DB_CONNECTION=sqlite 및 DB_DATABASE=:memory:를 설정하세요.
- 개발/운영 데이터를 건드리지 않도록 테스트용 별도 환경(.env.testing)을 유지하세요.
인가(Authorization) 테스트
use Illuminate\Support\Facades\Gate;
$this->assertTrue(Gate::forUser($user)->allows('update', $project));
$this->assertFalse(Gate::forUser($otherUser)->allows('update', $project));
Inertia 기능 테스트
Inertia.js를 사용하는 경우, Inertia 테스트 헬퍼를 사용하여 컴포넌트 이름과 props를 검증하세요.
use App\Models\User;
use Inertia\Testing\AssertableInertia;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
final class DashboardInertiaTest extends TestCase
{
use RefreshDatabase;
public function test_dashboard_inertia_props(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/dashboard');
$response->assertOk();
$response->assertInertia(fn (AssertableInertia $page) => $page
->component('Dashboard')
->where('user.id', $user->id)
->has()
);
}
}
테스트를 Inertia 응답에 맞게 유지하려면 원시 JSON 어설션보다 assertInertia를 권장합니다.