TDD 적용 범위
Test-Driven Development 적용 영역과 방법을 설명합니다.
개요
Multi-SaaS Kit은 Core 모듈과 비즈니스 로직에 TDD를 적용합니다. 테스트를 먼저 작성하고, 테스트를 통과하는 코드를 작성하는 방식으로 개발합니다.
┌────────────────────────────────── ───────────────────────────┐
│ TDD 사이클 │
│ │
│ ┌────────────────────────────────┐ │
│ │ │ │
│ ▼ │ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Red │────►│ Green │────►│Refactor │ │
│ │ (실패) │ │ (통과) │ │ (정리) │ │
│ └─────────┘ └─────────┘ └────┬────┘ │
│ │ │
│ └───────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
TDD 적용 영역
✅ TDD 필수 (MUST)
반드시 테스트를 먼저 작성해야 하는 영역입니다.
| 영역 | 이유 | 예시 |
|---|---|---|
| Core/Permission | 권한 오류 = 보안 사고 | UserLevel, HasLevel, Policy |
| Core/Tenant | 데이터 격리 = SaaS 핵심 | TenantScope, BelongsToTenant |
| Core/Audit | 감사 로그 무결성 | AuditLog, Auditable |
| 비즈니스 로직 | 계산, 상태 전이 | 구독, 결제, 제한 검사 |
| 보안 관련 | 인증, 인가 | Middleware, Policy, Guard |
// TDD 필수 영역 예시: 권한 검사
// 1. 먼저 테스트 작성
test('Tenant Admin은 자신의 테넌트 사용자만 관리할 수 있다', function () {
$tenantA = Tenant::factory()->create();
$tenantB = Tenant::factory()->create();
$adminA = User::factory()->create([
'tenant_id' => $tenantA->id,
'level' => 2, // Tenant Admin
]);
$userB = User::factory()->create([
'tenant_id' => $tenantB->id,
]);
$this->actingAs($adminA);
expect($adminA->canManageUser($userB))->toBeFalse();
});
// 2. 테스트 통과하는 코드 작성
public function canManageUser(User $target): bool
{
// 같은 테넌트인지 확인
if ($this->tenant_id !== $target->tenant_id) {
return false;
}
// 하위 레벨만 관리 가능
return $this->level < $target->level;
}
🔶 TDD 권장 (SHOULD)
테스트 작성을 권장하는 영역입니다.
| 영역 | 이유 | 예시 |
|---|---|---|
| Model 관계 | 데이터 무결성 | HasMany, BelongsTo |
| Service 클래스 | 로직 검증 | *Service.php |
| API 엔드포인트 | 계약 검증 | Controller, Resource |
| Seeder | 초기 데이터 검증 | CoreSeeder |
// TDD 권장 영역 예시: Service
test('SubscriptionService는 만료된 구독을 감지한다', function () {
$user = User::factory()->create();
$user->subscription()->create([
'expires_at' => now()->subDay(),
]);
$service = new SubscriptionService();
expect($service->isActive($user))->toBeFalse();
});
⚪ TDD 선택 (MAY)
복잡한 로직이 있을 때만 테스트를 작성합니다.
| 영역 | 언제 테스트 |
|---|---|
| Filament Resource | 복잡한 액션/필터 |
| View/Blade | 조건부 렌더링 로직 |
| 설정 파일 | 동적 설정 생성 |
❌ TDD 제외 (SKIP)
테스트가 불필요한 영역입니다.
| 영역 | 이유 |
|---|---|
| Laravel 기본 기능 | 프레임워크가 이미 테스트 |
| 단순 Getter/Setter | 로직 없음 |
| 마이그레이션 | 실행으로 검증 |
| 자동 생성 코드 | 생성기가 검증 |
TDD 워크플로우
1. Red 단계 (실패하는 테스트)
먼저 원하는 동작을 테스트로 정의합니다.
test('사용자는 자신의 테넌트 데이터만 볼 수 있다', function () {
// Given: 두 테넌트와 각 테넌트의 데이터
$tenantA = Tenant::factory()->create();
$tenantB = Tenant::factory()->create();
$userA = User::factory()->create(['tenant_id' => $tenantA->id]);
Product::factory()->create(['tenant_id' => $tenantA->id, 'name' => 'A']);
Product::factory()->create(['tenant_id' => $tenantB->id, 'name' => 'B']);
// When: 사용자 A로 조회
$this->actingAs($userA);
$products = Product::all();
// Then: 테넌트 A 데이터만 보여야 함
expect($products)->toHaveCount(1)
->and($products->first()->name)->toBe('A');
});
이 시점에서 테스트는 실패합니다 (Red).
2. Green 단계 (최소 구현)
테스트를 통과하는 최소한의 코드를 작성합니다.
// app/Core/Base/Tenant/Traits/BelongsToTenant.php
trait BelongsToTenant
{
public static function bootBelongsToTenant(): void
{
static::addGlobalScope(new TenantScope());
static::creating(function ($model) {
if (auth()->check() && auth()->user()->tenant_id) {
$model->tenant_id = auth()->user()->tenant_id;
}
});
}
}
테스트가 통과합니다 (Green).
3. Refactor 단계 (리팩토링)
코드를 개선하면서 테스트가 계속 통과하는지 확인합니다.
// 개선: 스코프 조건을 메서드로 분리
trait BelongsToTenant
{
public static function bootBelongsToTenant(): void
{
static::addGlobalScope(new TenantScope());
static::creating(fn ($model) => $model->assignTenantId());
}
protected function assignTenantId(): void
{
if ($this->shouldAssignTenant()) {
$this->tenant_id = auth()->user()->tenant_id;
}
}
protected function shouldAssignTenant(): bool
{
return auth()->check()
&& auth()->user()->tenant_id
&& !$this->tenant_id;
}
}
테스트가 여전히 통과합니다.
테스트 파일 위치
| 대상 코드 | 테스트 위치 |
|---|---|
app/Core/Base/Permission/* | tests/Unit/Core/Permission/* |
app/Core/Base/Tenant/* | tests/Unit/Core/Tenant/* |
app/Core/Base/Audit/* | tests/Unit/Core/Audit/* |
app/Models/* | tests/Unit/Models/* |
app/Services/* | tests/Unit/Services/* |
app/Http/Controllers/* | tests/Feature/Http/* |
테스트 명명 규칙
PHPUnit 스타일
// 형식: test_{주체}_{행위}_{결과}
public function test_tenant_admin_can_manage_users_in_same_tenant(): void
{
// ...
}
public function test_member_cannot_access_admin_panel(): void
{
// ...
}
Pest 스타일
// 한글 지원
test('Tenant Admin은 같은 테넌트 사용자를 관리할 수 있다', function () {
// ...
});
// it 문법
it('denies access to admin panel for members', function () {
// ...
});
AAA 패턴
테스트는 Arrange-Act-Assert 패턴을 따릅니다.
test('사용자 레벨 검사가 올바르게 동작한다', function () {
// Arrange (준비)
$tenantAdmin = User::factory()->create(['level' => 2]);
$member = User::factory()->create(['level' => 6]);
// Act (실행)
$canManage = $tenantAdmin->canManageUser($member);
// Assert (검증)
expect($canManage)->toBeTrue();
});
데이터 프로바이더
여러 케이스를 테스트할 때 사용합니다.
dataset('permission levels', [
'Platform Admin' => [0],
'SaaS Admin' => [1],
'Tenant Admin' => [2],
'Organization Admin' => [3],
'Workspace Admin' => [4],
'Group Leader' => [5],
'Member' => [6],
]);
test('상위 레벨은 하위 레벨을 관리할 수 있다', function (int $level) {
$superior = User::factory()->create(['level' => $level]);
$inferior = User::factory()->create(['level' => $level + 1]);
expect($superior->canManageUser($inferior))->toBeTrue();
})->with('permission levels')->skip(fn ($level) => $level === 6);
테스트 실행 확인
TDD 사이클에서 테스트 실행은 필수입니다.
# 특정 테스트 파일 실행
php artisan test tests/Unit/Core/Permission/HasLevelTest.php
# 특정 메서드만 실행
php artisan test --filter=test_tenant_admin_can_manage_users
# 실패한 테스트만 재실행
php artisan test --stop-on-failure
관련 문서
- 테스트 전략 개요 - 전체 테스트 시스템
- Unit 테스트 - 단위 테스트 작성법
- Feature 테스트 - 통합 테스트 작성법
- Permission 모듈 - 권한 테스트 예시