코드이그나이터4 마크다운 블로그 리팩토링 - 4 - 글 생성 컨트롤러 수정

고은연·2021년 11월 5일
0

글 생성 컨트롤러 수정하기

이번 챕터의 코드는 https://github.com/koeunyeon/ci4/commits/refacto-post-controller 에 있습니다.


엔티티 분리, 서비스 레이어 분리, 단위 테스트까지 마쳤으므로 우리는 잘 동작하는 서비스 기능이 있다는 것을 보증할 수 있게 되었습니다.
이제 글 생성 엔드포인트를 수정해서 비즈니스 로직은 모두 빼고 HTTP 레이어에 관련된 동작만 남겨두겠습니다.

먼저 app/Controllers/Post.php 파일의 add_input_markdown 메소드는 더이상 사용되지 않으므로 제거합니다.

생성 엔드포인트를 아래와 같이 수정합니다.
app/Controllers/Post.php

public function create()
{
    if (LoginHelper::isLogin() === false) {
        return $this->response->redirect("/post");
    }

    if ($this->request->getMethod() === "get") {
        return view("/post/create");
    }

    list($create_success, $post_id, $errors) =
        PostService::factory()
        ->create(
            $this->request->getPost(),
            LoginHelper::memberId()
        );
    if ($create_success){
        return $this->response->redirect("/post/show/$post_id");
    }
    return view("/post/create", [
        'post_data' => $this->request->getPost(),
        'errors' => $errors
    ]);
}

로그인 헬퍼를 제외한 모든 코드는 HTTP 계층 혹은 서비스만을 호출합니다. response->redirect, view, request->getPost 등은 HTTP 계층에 접근하는 컨트롤러 고유의 역할이므로 남겨둡니다.
이제 생성 엔드포인트의 흐름이 비즈니스의 흐름과 분리되었습니다.

use문에 use App\Services\PostService; 구문이 추가되었습니다.
use 문 중 use Michelf\Markdown은 사용하지 않으므로 삭제합니다.
현재까지의 use 문은 아래와 같습니다.

<?php
namespace App\Controllers;

use App\helpers\LoginHelper;
use CodeIgniter\Controller;

use App\Models\PostsModel;
use App\Services\PostService;

글 생성 컨트롤러 테스트하기

서비스 레벨의 테스트가 잘 끝났다고 해도, 실제로 컨트롤러가 잘 동작하는지에 대한 문제는 다릅니다.
컨트롤러는 저장 로직 말고도 로그인 여부 확인, GET/POST 메소드에 따른 동작 분기 등이 존재하죠. 이제 컨트롤러의 로직을 테스트해 보겠습니다.
/tests/controller/PostTests.php 파일을 생성하고 아래 내용으로 대체합니다.
/tests/controller/PostTests.php

<?php


namespace controller;


use App\Models\PostsModel;
use CodeIgniter\Test\FeatureTestCase;

class PostTests extends FeatureTestCase // (1)
{
    public function setUp(): void // (2)
    {
        parent::setUp();
    }

    public function tearDown(): void
    {
        parent::tearDown();
    }


    public function test_컨트롤러_글_생성_화면()   // (3)
    {
        $result = $this
            ->withSession(['member_id' => 1])  // (4)
            ->get("/post/create");  // (5)

        $result->assertOK();  // (6)
        $result->assertSee("글쓰기");  // (7)

    }

    public function test_컨트롤러_글_생성_로그인페이지_이동() // (8)
    {
        $result = $this->get("/post/create"); // (9)

        // $result->assertRedirect();  // (10)
        $result->assertStatus(302);

    }

    public function test_컨트롤러_글_생성_저장() // (11)
    {
        // given
        $post_data = [ // (12)
            'title' => '제목입니다.',
            'content' => '본문은 10글자 이상이죠?'
        ];

        // when
        $result = $this
            ->withSession(['member_id' => 1])
            ->post("/post/create", $post_data);  // (13)

        // then // (14)
        $result->assertOK();
        $result->assertStatus(302);

        // $redirectUrl = $result->getRedirectUrl(); // (15)
        $redirectUrl = $result->response->getHeaderLine('Location');
        $post_id = str_replace("/post/show/", "", $redirectUrl); // (16)

        // (17)
        $postModel = new PostsModel();
        $created_post = $postModel->find($post_id);
        $this->assertNotNull($created_post);
        $this->assertStringContainsString("제목입니다", $created_post['title']); // (18)
    }
}

(1) 컨트롤러 테스트는 FeatureTestCase 를 상속합니다. FeatureTestCase는 HTTP의 동작을 흉내냅니다. 참고로 컨트롤러 테스트는 실제 웹서버의 실행 여부와는 무관하게 테스트할 수 있습니다.

(2) setUp, tearDown은 테스트가 정상 실행되기 위한 기본 옵션입니다.

(3) 글 생성 화면을 테스트합니다. 글 생성 화면은 로그인 되어 있을 때와 로그인되어 있지 않을 때, 2가지 케이스가 존재하는데 , test_컨트롤러_글_생성_화면은 로그인되어 있을 때를 테스트합니다.

(4) withSession은 세션의 값을 설정하는 메소드입니다. 많은 로그인 구현은 세션을 사용하는데, 세션에 키를 설정함으로써 로그인한 것처럼 시뮬레이션합니다. 우리는 소셜 로그인을 사용했기 때문에 oAuth는 테스트하지 않고 oAuth를 통한 인증이 일어났다고 가정하고 테스트하는 것입니다.

(5) get 메소드는 HTTP GET 메소드를 시뮬레이션합니다. 파라미터는 URL입니다. 사실 $this->get(URL) 메소드는 $this->call("get", URL)의 별명입니다.

(6) 서비스를 테스트할 때 $this->assertOK를 사용했던 것과 비교해서 이번에는 $result->assertOK 를 사용했습니다. 이는 웹 요청을 시뮬레이션하는 컨트롤러 테스트의 특성상, 응답값을 그대로 사용하는 것이 편하기 때문입니다.
assertOK 메소드는 HTTP STATUS CODE 가 200-299 사이이거나, 300-399 사이이면서 응답 본문이 있는 경우 true를 반환합니다.

(7) assertSee 메소드는 HTTP 호출의 응답 내용 중 "글쓰기" 이라는 문자를 가지고 있으면 테스트를 통과한다는 뜻입니다.
우리는 지금 글쓰기 화면 테스트를 만들고 있습니다. HTML도 결국은 규칙이 있는 문자열이므로 문자열이 있는지 확인하면 응답이 원하는 대로 나오는지 확인할 수 있습니다.

(8) 로그인하지 않은 상태에서 로그인 페이지로 이동하는지 테스트하는 메소드입니다.

(9) 세션 설정 없이 곧바로 get 메소드를 사용했습니다.

(10) assertStatus는 HTTP STATUS CODE를 확인합니다. 코드이그나이터4 컨트롤러의 $this->response->redirect 메소드는 기본적으로 302 코드를 이용한 임시 리다이렉트 상태 코드를 반환하므로 정상적으로 리다이렉트되는지 확인합니다.
$result 변수의 타입인 FeatureResponse 클래스에는 assertRedirect 메소드가 있기 때문에 $this->assertRedirect() 로 테스트를 할 수 있다고 생각할 수도 있는데, 이 메소드는 코드이그나이터 4.0.4 버전에서는 정상 작동하지 않습니다. 이유는 내부적으로 구현된 isRedirect() 메소드가 RedirectResponse 타입의 인스턴스인지 여부로 assertRedirect()를 체크하는데, 의외로 코드이그나이터의 $this->response->redirect 메소드는 Response 타입의 인스턴스를 리턴할 뿐 RedirectResponse 타입을 리턴하지 않기 때문입니다. (버그인 것 같습니다.)
따라서 assertResult 는 주석 처리하고 HTTP STATUS CODE 로 판단하는 것입니다.

(11) 실제 저장이 잘 되는지도 확인해 봐야겠죠. 컨트롤러 레벨에서 글 생성 저장을 테스트하는 단위 테스트를 만듭니다.

(12) 글 데이터는 서비스 테스트에서 동일하게 가져왔습니다. HTTP POST로 전달되는 값입니다.

(13) post 메소드는 HTTP POST 메소드를 시뮬레이션합니다. get 메소드와 마찬가지로 call("post", URL) 의 별명입니다.

(14) 글 생성이 완료되면 글 보기 페이지로 리다이렉트되는 것을 확인합니다.

(15) HTTP 응답에서 상태 코드(HTTP STATUS CODE)가 301,302일 경우 이동할 페이지 주소가 응답 헤더 중 Location 으로 클라이언트에 전달됩니다. 따라서 응답 헤더 중 Location을 읽으면 이동할 주소를 알아낼 수 있습니다. (10)과 마찬가지 이유로 $result->getRedirectUrl() 메소드는 작동하지 않습니다.

(16) 방금 입력한 글 번호를 추출합니다. 글쓰기가 완료되고 나면 이동하는 주소는 /post/show/(글번호) 형식이기 때문에 /post/show/ 부분을 제거해 버리면 (글번호) 주소만 남습니다.

(17) 데이터가 잘 저장되었는지 확인하기 위해 데이터베이스를 조회해 봅니다.
서비스를 통해 데이터베이스에 접근하는 것이 아니라 직접 모델을 통해 조회하는 이유는 테스트의 신뢰성을 높이기 위함입니다. 만약 서비스 클래스를 통해 단위 테스트를 작성했는데 서비스 클래스의 구현이 달라져서 테스트가 깨질 경우 컨트롤러의 테스트도 함께 깨지는 경우가 생기기 때문입니다.

(18) assertStringContainsString 메소드는 문자열을 포함하는지를 나타내는 단언문입니다. 파라미터 순서는 needle, haystack 으로 haystackneedle이 포함되어 있는지를 검사합니다.


이제 테스트를 실행해 봅시다.

php phpunit.phar --testdox --verbose  tests/controller/PostTests.php

결과가 잘 나오네요.

Post Tests (controller\PostTests)
 ✔ 컨트롤러 글 생성 화면  141 ms
 ✔ 컨트롤러 글 생성 로그인페이지 이동  55 ms
 ✔ 컨트롤러 글 생성 저장  77 ms

Time: 00:00.342, Memory: 22.00 MB

OK (3 tests, 7 assertions)
profile
중년 아저씨. 10 + n년차 백엔드 개발자. 스타트업과 창업, 솔로프리너와 1인 기업에 관심 많아요.

0개의 댓글