본문으로 건너뛰기

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

관련 문서