107호. 모델 테스트 어떻게 해야할까? free

2020-01-03


모델 테스트 어떻게들 하고 계신가요?


다음은 라라벨 매뉴얼 중 쿼리 스코프 섹션에 나오는 예제입니다.


class User extends Model
{
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}
}

이런 코드를 모델에 추가했을 때 어떻게 테스트를 해야할까요? 아래 세 가지 선택지 중 하나를 선택할 수 있을 것 같습니다.



  1. 테스트하지 않는다.

  2. 기능 테스트(혹은 통합 테스트)를 한다. DB에 값을 넣고 정확히 동작하는지 확인한다.

  3. 단위 테스트를 한다. User 클래스를 부분적으로 모킹해서 where 매서드가 호출되는지 확인한다.


잘못되면 애플리케이션을 망가뜨릴 수도 있기 때문에 테스트를 하지 않는 것보단 하는 것이 나은 것 같습니다. 그렇다면 2번과 3번 중 하나를 선택해야 하겠네요.


라라벨 매뉴얼에서는 Feature 테스트와 Unit 테스트를 다음과 같이 설명하고 있습니다.



기본적으로, 애플리케이션의 tests 디렉토리는 두개의 디렉토리: Feature 와 Unit 를 가지고 있습니다. 단위테스트는 코드의 매우 작고, 독립적인 부분에 초점을 둔 테스트 입니다. 실제로, 대부분의 단위 테스트는 하나의 메소드에 포커스를 맞춥니다. 기능 테스트는 여러 객체가 서로 상호작용하는 방식 또는 JSON 엔드 포인트에 대한 전체 HTTP 요청을 포함하여 코드의 많은 부분을 테스트할 수 있습니다. - 테스팅:시작하기



이 설명에 따르면 모델의 매서드 하나를 테스트하는 것이기 때문에 단위 테스트를 작성하면 될 것 처럼 느껴집니다. 따라서 3번이 정답으로 보입니다.


하지만 scopePopular() 매서드는 단지 엘로퀀트 모델의 매서드를 한 번 감싸서 읽기 좋게 만든 것에 불과합니다. where 매서드가 호출되었는지를 확인한다고 해서 실제로 우리가 원하는 "테스트"가 되진 않습니다.


scopePopular()와 같이 엘로퀀트의 쿼리 기능을 사용하는 매소드는 데이터베이스와 땔래야 땔 수 없는 관계이기 때문에 태생적으로 '독립적'일 수 없습니다. 단일 매서드를 테스트하는 것이라 단위 테스트처럼 보이지만 단위 테스트에 어울리지 않는 것이지요.


'아니 단위 테스트가 불가능하다니! 데이터베이스 의존성을 끊지 못하다니!! 뭔가 잘못된 것 아닌가!!!' 이렇게 생각하실 수도 있습니다. 당연히 틀린 생각은 아니라고 생각합니다. 데이터베이스에 강하게 결합되지 않도록 리팩토링을 하겠다고 결정할 수도 있습니다. 하지만 그렇다고 해도 결국은 애플리케이션이 제대로 기능하는지 확인하기 위해 데이터베이스를 사용하는 기능 테스트 혹은 통합 테스트가 필요합니다.


그러니 모델에 추가한 매서드가 데이터베이스를 건드린다면 단위 테스트가 아닌 기능 테스트로 작성하세요. 물론 데이터베이스와 관련없는 매서드들은 단위 테스트로 작성하시고요. :)


scopePopular() 매서드를 기능 테스트로 작성하면 아래와 같이 아주 간단하게 작성하실 수 있습니다.


<?php

namespace Tests\Feature;

use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class SubscriptionTest extends TestCase
{
use RefreshDatabase;

/**
* @see User::scopePopular()
* @test
*/
public function scopePopular()
{
$user = factory(User::class)->create([
'vote' => 100
]);

$popularUser = factory(User::class)->create([
'vote' => 101
]);

$users = User::popular()->get();

$this->assertCount(1, $users->count());
$this->assertEquals($popularUser->id, $users->first()->id);
}

마치며


사실 고민의 시작은 '유닛 테스트를 해야하는데 엘로퀀트 모델이 데이터베이스와 강하게 결합되어 있어서 어떻게 해야하지?' 였어요. 기능 테스트는 HTTP 요청이나 콘솔 매서드를 사용하는 것이고, 단일 매서드 테스트는 유닛 테스트를 하는 거라고 단순하게 생각했기 때문에 엉뚱한 질문을 하게 된 것이죠. 엄청 고민하고 자료도 찾아봤는데 해답은 너무나 단순하게도 유닛 테스트가 아닌 기능 테스트를 하면 된다였어요.


결정적으로 확신을 한건 라라벨 4 시절에 출간되었던 제프리 웨이의 'Laravel Testing Decoded'를 다시 들춰보고 난 후였어요. 정확히 이 상황에 대해 언급해두었더라고요. 여러가지 의견이 있을 수 있지만 자신은 통합 테스트를 권한다고 했습니다.(기능 테스트는 통합 테스트의 일종이라 그냥 혼용했습니다) 제프리 웨이가 다른 개발자들에게 의견을 묻기도 했는데 통합 테스트가 대세이기도 했어요.


한 주 수고 많으셨습니다~

즐거운 주말 되세요!


1일 1식 라라벨 107호

2020년 1월 3일


이현석

메쉬 코리아 개발자. 바쁜 팀장님 대신 알려주는 신입 PHP 개발자 안내서를 쓰고, 클린 아키텍처 인 PHP를 번역했습니다. 2020년에 출간될 Laravel Up & Running 2nd Edition을 번역하고 있습니다.