리팩토링 없이 레거시 Laravel 코드 테스트
Source: Dev.to
실제 PHP 프로젝트를 위한 실용적인 전략
레거시 코드베이스는 피할 수 없는 현실입니다. 대부분은 그린필드 프로젝트에 참여하지 못하고, 수년 동안 기능마다 기능마다 자연스럽게 성장한 애플리케이션을 인계받게 됩니다. 테스트 커버리지는 거의 없고, 아키텍처 규율은 더더욱 부족한 경우가 많죠. 그럼에도 불구하고 우리는 테스트가 필요합니다: 회귀를 방지하고, 안전하게 리팩터링하며, 자신감을 가지고 기능을 제공하기 위해서입니다.
문제는 명확합니다: 테스트를 위해 설계되지 않은 코드를 어떻게 시작해서 테스트할 것인가? 이 글에서는 레거시 Laravel(및 PHP) 코드를 먼저 리팩터링하지 않고 테스트하는 실전·전투 검증된 전략을 소개합니다. 이 기법들은 복잡하고 현실적인 코드베이스에 바로 적용할 수 있습니다. 구체적인 예시를 통해 살펴보겠습니다.
실제 레거시 예시
아래는 6년 된 Laravel 코드베이스에서 흔히 볼 수 있는, 단순화했지만 현실적인 컨트롤러 메서드입니다:
public function processOrder(Request $request)
{
$order = new Order();
$order->product_id = $request->product_id;
$order->quantity = $request->quantity;
$order->user_id = auth()->id();
$order->status = 'pending';
$order->save();
// business rules mixed directly inside controller
if ($order->quantity > 10) {
$order->priority = 'high';
}
if ($order->product_id === 999) {
// fetch external pricing
$response = Http::post('https://external-api.com/prices', [
'product_id' => $order->product_id,
]);
if ($response->ok()) {
$order->external_price = $response->json('price');
}
}
// logging inside business logic
Log::info('Order processed', [
'order_id' => $order->id,
'user' => auth()->id(),
]);
// send email
Mail::to(auth()->user())->send(new OrderCreatedMail($order));
// update stock
$product = Product::find($order->product_id);
$product->stock -= $order->quantity;
$product->save();
return response()->json([
'id' => $order->id,
'status' => $order->status,
'priority' => $order->priority ?? 'normal',
], 201);
}
왜 이 메서드는 테스트하기 어려운가
- 여러 부수 효과(HTTP 요청, 이메일, 로깅)
- 혼합된 관심사(재고 업데이트, 비즈니스 규칙, 외부 API 호출)
- 전역 상태(
auth()) - 의존성 주입이 없음
- 데이터베이스 작업이 흐름 제어에 밀접하게 결합
- 컨트롤러가 너무 많은 일을 함
코드를 건드리지 않고 테스트하기
컨트롤러 메서드를 블랙 박스로 취급하세요; 내부 구조를 테스트할 필요는 없습니다.
예시 테스트
public function test_it_processes_a_basic_order()
{
Mail::fake();
Http::fake();
$user = User::factory()->create();
$this->actingAs($user);
$product = Product::factory()->create(['stock' => 50]);
$response = $this->postJson('/api/orders/process', [
'product_id' => $product->id,
'quantity' => 3,
]);
$response->assertStatus(201);
$this->assertDatabaseHas('orders', [
'product_id' => $product->id,
'quantity' => 3,
'user_id' => $user->id,
]);
$this->assertDatabaseHas('products', [
'id' => $product->id,
'stock' => 47,
]);
Mail::assertSent(OrderCreatedMail::class);
}
이 테스트는 원본 코드를 수정하지 않고도 다음 여섯 가지를 검증합니다:
- 주문이 생성됨.
- 인증된 사용자에게 귀속됨.
- 재고가 올바르게 감소함.
- 애플리케이션이 올바른 HTTP 상태 코드를 반환함.
- 반환된 JSON에 기대한 필드가 포함됨.
- 이메일이 전송됨.
최소한의 마찰로 전체 통합 커버리지를 제공합니다.
외부 HTTP 호출 가짜 처리
레거시 컨트롤러는 product_id === 999일 때 외부 엔드포인트를 호출합니다. Laravel의 Http::fake()를 사용하면 안전하게 가짜 처리할 수 있습니다:
Http::fake([
'external-api.com/*' => Http::response([
'price' => 123.45,
], 200),
]);
이제 external_price를 계산하는 로직을 테스트할 수 있습니다:
$response = $this->postJson('/api/orders/process', [
'product_id' => 999,
'quantity' => 1,
]);
$this->assertDatabaseHas('orders', [
'external_price' => 123.45,
]);
리팩터링이 전혀 필요하지 않습니다.
전역 변수 오버라이드
메서드가 auth()를 직접 사용합니다. Laravel은 테스트에서 인증된 사용자를 쉽게 교체할 수 있게 해줍니다:
$this->actingAs($user);
코드가 현재 시간을 사용한다면, 시간을 고정할 수 있습니다:
Date::setTestNow('2025-01-01 10:00:00');
고집스러운 부수 효과에 대한 부분 모킹
다음과 같이 테스트에서 접근하기 어려운 호출이 있다면:
$this->syncInventoryWithWarehouseApi($order);
부분 모크로 해당 부분만 교체할 수 있습니다:
$controller = Mockery::mock(OrderController::class)->makePartial();
$controller->shouldReceive('syncInventoryWithWarehouseApi')
->andReturn(true);
실제 컨트롤러 로직은 그대로 두면서 문제 부분만 가짜 처리합니다.
복잡한 응답에 대한 스냅샷 테스트
JSON 응답이 크거나 동적 키를 포함할 경우, 스냅샷 테스트가 어설션을 단순화합니다:
$response = $this->postJson('/api/orders/process', [...]);
$this->assertMatchesJsonSnapshot($response->json());
구조가 의도치 않게 변경되면 테스트가 즉시 실패합니다.
지저분한 Laravel 코드를 위한 반복 가능한 프로세스
- 간단한 해피패스 통합 테스트를 작성한다.
- 모든 부수 효과를 가짜 처리한다: HTTP, 메일, 스토리지, 이벤트 등.
- 내부 디테일 대신 데이터베이스 상태를 어설션한다.
- 적어도 하나의 엣지 케이스 테스트를 추가한다.
- 커버리지가 확보된 뒤에만 리팩터링을 고려한다.
- 시스템을 하나씩 안정화한다.
코드를 “고쳐야” 테스트할 필요는 없습니다.
마무리 생각
레거시 Laravel 코드를 테스트하는 것은 우아함을 위한 것이 아니라 위험을 줄이고, 안전성을 높이며, 아직 완전히 신뢰하지 못하는 코드에 대한 자신감을 구축하기 위한 것입니다. 위에 보여진 컨트롤러는 지저분하지만, 다음과 같은 접근을 하면 충분히 다룰 수 있습니다:
- HTTP를 통한 테스트.
- 가짜(Fake)로 부수 효과 격리.
- 전역 변수 제어.
- 필요할 때 부분 모크 사용.
- 복잡한 응답은 스냅샷 테스트.
이러한 기법을 통해 어제의 코드를 안정화하면서 내일의 리팩터링을 위한 견고한 기반을 마련할 수 있습니다.