질문을 삭제하지 말아주세요.!
 
1
0
-1

라라벨 자체에서 phpunit을 쉽게 작성할 수 있도록 추가적인 assert을 많이 제공하고 있습니다. 이런 assert를 사용하기 위해서는 `call()` 메소드를 사용해서 `TestResponse`를 받아야 하는데요. 이 기능을 사용하면서 보니 이 접근은 컨트롤러의 단위 테스트를 작성한다기보다 프레임워크 전체에 걸친 통합 테스트를 작성하는 방식이 되는 것 같습니다. 라라벨 문서 용어를 빌리면 feature test가 되는데요.

생각하는 유닛 테스트는 대략 이렇습니다.

<?php
use Illuminate\Foundation\Testing\RefreshDatabase;

public function testContentPage()
{
    // Arrange
    $page = factory(Page::class)->make();
    $page->save();
    $expected = Page::find($page->id);

    // Act
    $ctrl = $this->app->make(PageController::class);
    $result = $ctrl->detail($page->name);
    $viewName = $result->getName();
    $viewData = $result->getData();

    // Assert
    $this->assertEquals('page.default', $viewName);
    $this->assertEquals([
        'page' => $expected,
    ], $viewData);
}

feature 테스트는 이렇게 작성했습니다.

<?php
Route::get('/{name?}', 'PageController@detail')
    ->where('name', '[A-Za-z0-9\-\_]+')
    ->name('page');

use Illuminate\Foundation\Testing\RefreshDatabase;


public function testContentPage()
{
    // Arrange
    $page = factory(Page::class)->make();
    $page->save();
    $expected = Page::find($page->id);

    // Act
    $response = $this->call('GET', "/{$expected->name}");

    // Assert
    $response
        ->assertStatus(200)
        ->assertViewIs('page.default')
        ->assertViewHasAll([
            'page' => $expected,
        ]);
}

그런데 테스트에 관한 글을 찾아보면 대부분 후자 방식으로 작성하고 있었습니다. 전자 방식으로는 아예 작성을 하지 않나 궁금하더라고요. 후자 방식으로 작성했을 때는 이런 문제가 있었습니다.

  • 컨트롤러 테스트를 위해서 router가 필수적으로 선언되어야 하며 라우트의 세부 내용을 테스트가 알아야 함
  • detail()이 'page' 외에 새로운 값을 추가적으로 반환하는 경우에도 테스트가 실패하지 않고 통과함

컨트롤러의 역할이 프레임워크의 라이프사이클과 분리하기 어려울 정도로 의존적이면 그것도 나쁜 냄새가 아닌가 싶기도 한데 전자 방식으로 작성하면 Request가 필요한 경우에는 코드가 좀 장황해지는 경향이 있던 것 같습니다. 테스트를 둘 다 작성해야 가장 이상적일 것 같긴 합니다만 비슷한 테스트 로직이 중복되는 것 같기도 해서 고민이 됩니다.

컨트롤러를 어떤 방식으로 테스트하고 계신지 궁금합니다.

    CommentAdd your comment...

    3 answers

    1.  
      2
      1
      0

      제가 질문을 정확히 파악했는지 모르겠습니다. 그럼에도 불구하고, 제 의견은 "컨트롤러는 단위 테스트하지 않는다" 입니다. 

      HTTP 컨트롤러, 콘솔 커맨드, 큐/잡 핸들러등은 메인 영역에 머물면서, 여러가지 API들을 조합해서 흐름 제어(flow control)를 하는 최상위 함수같은 녀석이고, 딜리버리 메커니즘에 강하게 엮여 있고, 서비스/모델등 코어 레이어와도 엮여 있으므로, 완벽하게 단위 테스트하기가 불가하다고/무의미하다고 생각하기 때문입니다. 즉, 컨트롤러는 애플리케이션이 제공하는 기능(feature) 자체이므로, 의존성 격리를 통한 단위 테스트는 불필요하다 생각합니다.

        CommentAdd your comment...
      1.  
        2
        1
        0

        전에 라라벨4 시절에 제프리웨이가 쓴 책에서는 죄다 mocking 해서 테스트하는 것 같았어요.

        마치, 컨트롤러가 사용하는 모든 객체는 이미 다 테스트 된거니까, 잘 호출하는지만 체크한다는 느낌이었습니다.

        근데 최근 제프리웨이의 TDD 영상에서는 컨트롤러는 테스트하지 않고 그냥 feature 테스트만 하더라구요. 귀찮아서 그런건지, 입장이 바뀐건지는 모르겠어요. 라라벨 4 당시의 책에서는 컨트롤러도 테스트하라고 했었거든요. 

        1. 용균

          현석님 답변 읽어보니 파사드 때문인가 싶기도 하네요. 파사드를 사용하고 있으면 유닛테스트가 좀 까다로워지는 것 같더라고요. 의존성 주입에도 그렇고, 일단 서비스 로케이터 패턴처럼 동작하니까 프레임워크 전체의 맥락이 없을 때는 좀 애매해지는 경향도 있는 것 같고요. 제프리웨이한테 물어봐야 할까요 ㅎㅎ

        CommentAdd your comment...
      2.  
        2
        1
        0

        저는 컨트롤러를 테스트한다는 것 자체가 이미 유닛테스트와는 거리가 있지 않나..라는 생각이 떠나지 않는데요.

        '컨트롤러의 역할이 프레임워크의 라이프사이클과 분리하기 어려울 정도로 의존적'이란 말씀을 정확히는 이해는 못했습니다만,

        컨트롤러의 역할에 비추어보면 이건 통합테스트(라라벨 식이라면 feature테스트)의 관점에서 봐야하지 않나 생각이 듭니다.

        저도 다른 분들 의견이 궁금하네요!


        1. 용균

          중간에 인용하신 부분은 뒤에 바로 말씀하신 대로 컨트롤러 역할에 대한 맥락으로 적었습니다.

          저도 좀 더 생각해보긴 했는데 둘 다 작성해야 하지 않나 쪽으로 많이 기울긴 했습니다. feature 테스트를 수행하면 요청을 넣고 응답을 검사하는 방식이니 컨트롤러를 간접적으로 검사를 하게 되니까요. 컨트롤러의 테스트를 feature 테스트로만 수행했을 경우에는 컨트롤러의 구현은 그대로라도 미들웨어 구성이 달라져서 테스트 실패가 발생할 수 있기도 하고요.

          결국 둘 다 목적과 필요에 따라 작성해야 하는게 뭔가 정석같은 답인 것 같기도 합니다.. 어렵네요 🤔

        CommentAdd your comment...