테스트는 코드가 예상대로 정확하게 동작하는지를 자동으로 검증하는 과정이다.
이는 버그를 줄이고, 미래에 코드를 변경하는 것에 대한 자신감을 주는 매우 중요한 활동이다.
훌륭한 곡예사는 안전망 없이도 공연을 할 수 있지만, 안전망(테스트)이 있기 때문에 더 대담하고 새로운 기술에 자신감 있게 도전할 수 있다.
마찬가지로, 개발자는 테스트 코드가 있기 때문에 새로운 기능을 추가하거나 기존 코드를 리팩토링(개선)할 때, "혹시 다른 기능이 고장 나지 않았을까?" 라는 불안감 없이 자신감 있게 코드를 변경할 수 있다.
라라벨은 기본적으로 두 가지 종류의 테스트를 지원하며, tests 폴더 안에 각각 Unit과 Feature라는 하위 폴더로 구분되어 있다.
유닛 테스트 (Unit Tests - 부품 검사)
애플리케이션의 가장 작은 단위(예: 클래스의 특정 메서드 하나)를 독립적으로 검사하는 테스트이다.
"이 calculatePrice() 메서드에 1000원과 10% 할인율을 넣으면, 정확히 900원이 반환되는가?" 와 같이 매우 작고 고립된 부분을 테스트한다.
기능 테스트 (Feature Tests - 완제품 검사)
사용자의 행동처럼 여러 부분이 함께 동작하는 전체 흐름을 테스트한다.
"사용자가 /tasks 주소로 POST 요청을 보내면, 실제로 데이터베이스에 새로운 task가 생성되고, 성공했다는 JSON 응답이 돌아오는가?" 와 같이 API 엔드포인트나 웹 페이지의 전체 동작을 검증한다.
백엔드 개발에서는 주로 기능 테스트를 작성하게 된다.
php artisan make:test 명령어로 새로운 테스트 파일을 생성한다.# 기능 테스트 생성
php artisan make:test TaskApiTest
# 유닛 테스트 생성
php artisan make:test TaskTest --unit
이 명령어는 tests/Feature/TaskApiTest.php 파일을 생성한다.
테스트 코드 작성 (tests/Feature/TaskApiTest.php)
테스트 메서드는 보통 test_ 또는 @test 어노테이션으로 시작한다.
메서드 이름은 무엇을 테스트하는지 명확하게 짓는 것이 중요하다.
준비 (Arrange): 테스트에 필요한 데이터를 준비한다. (예: 사용자 생성)
실행 (Act): 실제 테스트할 행동을 실행한다. (예: API 요청)
단언 (Assert): 실행 결과가 우리가 예상한 것과 같은지 확인한다.
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\Task;
use App\Models\User;
class TaskApiTest extends TestCase
{
// 테스트 실행 시마다 데이터베이스를 초기화해주는 유용한 기능
use RefreshDatabase;
/** @test */
public function a_user_can_get_a_list_of_their_tasks()
{
// 1. 준비 (Arrange)
// 테스트용 사용자를 한 명 만들고, 그 사용자의 할 일을 2개 만든다.
$user = User::factory()->create();
Task::factory()->count(2)->create(['user_id' => $user->id]);
// 2. 실행 (Act)
// 해당 사용자로 로그인한 것처럼 API에 GET 요청을 보낸다.
$response = $this->actingAs($user)->getJson('/api/tasks');
// 3. 단언 (Assert)
// 응답 상태 코드가 200 (성공)인지 확인한다.
$response->assertStatus(200);
// 응답으로 받은 JSON 데이터가 정확히 2개인지 확인한다.
$response->assertJsonCount(2);
}
}
tests 폴더 안의 모든 테스트를 자동으로 실행하고 결과를 보여줍니다.php artisan test
특정 파일만 테스트하고 싶다면 파일 경로를 추가할 수 있다:
php artisan test tests/Feature/TaskApiTest.php
테스트를 작성할 때마다 Task::create(['title' => ...]) 처럼 테스트 데이터를 수동으로 만드는 것은 번거롭다.
팩토리(Factory)는 실제 데이터처럼 보이는 가짜 테스트 데이터를 대량으로, 그리고 손쉽게 생성해주는 강력한 도구이다.
database/factories/TaskFactory.php):Faker라는 라이브러리를 사용하여 진짜 같은 가짜 데이터를 만들 수 있다.public function definition()
{
return [
'user_id' => User::factory(),
'title' => $this->faker->sentence(), // 가짜 문장
'completed' => $this->faker->boolean(), // true 또는 false
];
}
// Task 1개 생성
$task = Task::factory()->create();
// 특정 user에 속한 Task 10개 생성
$user = User::factory()->create();
$tasks = Task::factory()->count(10)->create(['user_id' => $user->id]);
// 완료된 상태의 Task 1개 생성
$completedTask = Task::factory()->completed()->create(); // '상태(State)' 기능 활용
외부 API(예: 결제 시스템, 날씨 정보 API)와 통신하는 코드를 테스트해야 할 때, 실제 API를 호출하는 것은 느리고, 비용이 발생하며, 불안정하다.
이때 모킹(Mocking)이나 페이킹(Faking)을 사용하여 외부 서비스의 행동을 흉내 내는 '가짜 객체'를 만들어 테스트를 안정적으로 만든다.
역할:
"실제 결제사에 요청을 보내는 대신, '성공'이라는 응답을 보냈다고 가정하고 테스트하자."
라라벨의 페이킹 기능: 라라벨은 Mail, Notification, Queue, Http 등 많은 내장 기능에 대해 간편한 페이킹 메서드를 제공한다.
use Illuminate\Support\Facades\Http;
/** @test */
public function it_fetches_weather_data_correctly()
{
// Http 클라이언트를 가짜로 대체하고, 특정 URL 요청에 대해 가짜 응답을 설정
Http::fake([
'api.weather.com/*' => Http::response(['temperature' => 25], 200),
]);
$response = $this->get('/api/weather');
$response->assertOk();
$response->assertJson(['temp_celsius' => 25]);
// 실제로 api.weather.com 에 요청이 갔는지도 검증할 수 있음
Http::assertSent(function ($request) {
return $request->url() == 'https://api.weather.com/seoul';
});
}
테스트는 서로에게 영향을 주지 않고 독립적으로 실행되어야 한다.
RefreshDatabase 트레이트를 사용하면, 각각의 테스트 메서드가 실행되기 전에 데이터베이스를 깨끗한 초기 상태로 되돌려준다.
TDD는 코드를 작성하는 방식에 대한 하나의 개발 방법론이다.
"실제 기능 코드를 작성하기 전에, 그 기능에 대한 실패하는 테스트 코드를 먼저 작성한다"는 것이 핵심이다.
개발 순서:
Red: 실패할 것을 예상하고 테스트 코드를 먼저 작성한다. (php artisan test -> 실패)
Green: 이 테스트를 통과할 만큼의 최소한의 실제 코드를 작성한다. (php artisan test -> 성공)
Refactor: 코드를 개선하고 정리한다. (테스트는 계속 성공 상태 유지)
Pest는 라라벨의 기본 테스트 프레임워크인 PHPUnit 위에 만들어진, 더 읽기 쉽고 표현적인 문법을 제공하는 인기 있는 테스트 프레임워크이다.
public function test_a_user_can_get_tasks() { ... }
test('a user can get their tasks', function () { ... });
// it('allows a user to get their tasks', function () { ... });
마치 일반적인 문장처럼 테스트 코드를 작성할 수 있어 가독성을 중요하게 생각하는 개발자들에게 인기가 많다.
마지막 한 걸음
테스트 커버리지 (Test Coverage) - 내 코드의 건강 검진표
테스트 커버리지는 작성한 테스트 코드가 실제 애플리케이션 코드의 몇 퍼센트를 실행했는지를 측정하는 지표이다.
이를 통해 테스트하지 않은 '사각지대'를 찾아낼 수 있다.
- 실행 방법:
# --coverage 옵션을 추가하면 테스트 실행 후 커버리지 리포트를 보여준다. php artisan test --coverage
- 주의할 점:
100% 커버리지를 맹목적으로 추구할 필요는 없다.
중요한 비즈니스 로직과 복잡한 조건문 위주로 높은 커버리지를 유지하는 것이 더 효율적이다.병렬 테스트 (Parallel Testing) - 속도의 혁신
애플리케이션이 커지면 테스트의 개수도 수백, 수천 개로 늘어나 모든 테스트를 실행하는 데 몇 분씩 걸릴 수 있다.
병렬 테스트는 여러 CPU 코어를 사용하여 여러 테스트를 동시에 실행함으로써 이 시간을 획기적으로 단축시켜 준다.
- 실행 방법:
# --parallel 옵션을 추가하면 알아서 테스트를 분산 실행한다. php artisan test --parallel대규모 프로젝트에서는 CI/CD 파이프라인의 시간을 절약해 주는 필수적인 기능이다.
브라우저 테스트 (Browser Testing) - Dusk
지금까지 배운 기능 테스트는 HTTP 요청을 시뮬레이션하는 백엔드 중심의 테스트였다.
만약 자바스크립트로 구현된 프론트엔드 UI(버튼 클릭, 폼 입력 등)까지 실제 크롬 브라우저를 조종하여 테스트하고 싶다면, 라라벨 Dusk를 사용한다.
- 역할:
"로그인 버튼을 누르고, 아이디/비밀번호를 입력한 다음, '로그인' 버튼을 클릭했을 때, 실제로 대시보드 페이지로 이동하는가?" 와 같은 사용자 시나리오를 자동화한다.
테스트는 단순히 버그를 찾는 행위를 넘어, 소프트웨어의 품질과 안정성을 보장하는 가장 근본적인 활동이다.
테스트 코드는 다음과 같은 가치를 제공하는 살아있는 문서이다.
신뢰성:
내 코드가 의도한 대로 동작한다는 것을 증명한다.
유지보수성:
코드를 수정한 후에도 기존 기능이 고장 나지 않았다는 자신감을 준다.
협업:
다른 개발자가 내 코드를 쉽게 이해하고 안전하게 수정할 수 있도록 돕는다.