코드이그나이터4 마크다운 블로그 리팩토링 - 8 - 테스트! 테스트! 테스트!

테스트! 테스트! 테스트!

이번 챕터의 글은 https://github.com/koeunyeon/ci4/commits/refacto-test 에 있습니다.

테스트를 합시다.

글 기능을 전반적으로 수정했으니 테스트가 필요하겠죠.
우선 직접 눌러가면서 테스트하는 엔드 투 엔드 테스트는 반드시 진행하세요.
그리고 나면, 이제 테스트 자동화를 위한 단위 테스트를 작성해야겠죠.
수정한 기능들에 대해서 단위 테스트를 작성하겠습니다.

서비스 테스트 작성하기

먼저 서비스 테스트 코드를 작성합니다.
tests/service/PostTests.php

<?php

namespace service;

use App\Services\PostService;
use CodeIgniter\Test\CIDatabaseTestCase;

class PostTests extends CIDatabaseTestCase
{
    private $postService; // (1)
    public function setUp(): void
    {
        parent::setUp();
        $this->postService = PostService::factory(); // (2)
    }

    ... ( 생략 ) ...

    private function insert_post() // (3)
    {
        $post_data = [
            'title' => '제목입니다.',
            'content' => '본문은 10글자 이상이죠?'
        ];

        $memberId = 1;

        list($result, $post_id, $errors) = $this->postService->create($post_data, $memberId); // (4)
        return [$result, $post_id, $errors];
    }

    public function test_글_조회()
    {
        // given
        list($result, $post_id, $errors) = $this->insert_post();

        // when
        $post_success = $this->postService->find($post_id);
        $post_fail = $this->postService->find(-1);

        // then  // (5)
        $this->assertNotNull($post_success);
        $this->assertNull($post_fail);
    }

    private function setup_post(){ // (6)
        list($result, $post_id, $errors) = $this->insert_post();
        return $this->postService->find($post_id);
    }

    public function test_작성자_확인()
    {
        // given
        $post = $this->setup_post();

        // when  // (7)
        $author_true = $post->isAuthor(1);
        $author_false = $post->isAuthor(-1);

        // then  // (8)
        $this->assertTrue($author_true);
        $this->assertNotTrue($author_false);
    }

    public function test_수정()
    {
        // given
        $post = $this->setup_post();
        $new_post_data = ['title'=> '새로운 제목이에요'];

        // when  // (9)
        list($updateSuccess, $errors) = $this->postService->update($post, $new_post_data);

        // then  // (10)
        $this->assertTrue($updateSuccess);

 $updated_title = $this->postService->find($post->post_id)->title;
 $this->assertEquals($new_post_data['title'], $updated_title);
    }

    public function test_삭제()
    {
        // given   // (11)
        list($result, $post_id, $errors) = $this->insert_post();

        // when   // (12)
        $not_deleted = $this->postService->delete($post_id, -1);
        $deleted = $this->postService->delete($post_id, 1);

        // then   // (13)
        $this->assertNotTrue($not_deleted);
        $this->assertTrue($deleted);
    }

    public function test_목록()
    {
        // given
        foreach (range(1,5) as $item) { // (14)
            $this->insert_post();
        }

        // when // (15)
        list($pager, $post_list) = $this->postService->post_list(1);

        // then // (16)
        $this->assertNotNull($pager);
        $this->assertNotNull($post_list);
        $this->assertCount(5, $post_list);
    }

    public function test_목록_2페이지()
    {
        // given
        foreach (range(1,5) as $item) {
            $this->insert_post();
        }

        // when // (17)
        list($pager, $post_list) = $this->postService->post_list(2);

        // then // (18)
        $this->assertCount(0, $post_list);
    }
}

(1) 서비스 객체를 테스트함으로 PostService 클래스의 인스턴스 $postService는 항상 필요합니다. 서비스 객체는 상태를 가지지 않기 때문에 공통으로 사용할 수 있죠. 공통으로 사용하기 위해 멤버 변수로 선언합니다.

(2) setUp() 메소드는 단위 테스트가 실행될 때 먼저 불립니다. 그러므로 초기화 할 객체가 있다면 setUp()에서 하면 됩니다. 반면 정리해야 할 리소스는 tearDown()에서 하면 되죠.

(3) 글 조회, 수정, 삭제는 글 데이터가 있다는 전제 하에서만 작동합니다. insert_post 메소드는 글 데이터를 미리 준비하기 위해 따로 분리해 놓은 메소드입니다. 데이터베이스에 임의의 데이터를 입력하죠. test_로 시작하지도 않고, 한글 이름도 아니며, 심지어는 private입니다. 따라서 PHPUnit은 insert_post 메소드를 단위 테스트용 메소드로 인식하지 않습니다.

(4) 임의의 데이터를 입력합니다. 이 때 (1)에서 선언한 $this->postService 객체를 사용합니다.

(5) $post_success 는 글이 있을 때를 테스트한 결과이고, $post_fail 변수는 글이 없을 때를 테스트한 변수입니다.

(6) setup_post 메소드도 (3)insert_post 처럼 미리 데이터를 준비합니다. 대신 데이터 입력만 하는 것이 아니라 입력한 데이터의 객체도 사용할 수 있게 준비하는 메소드입니다.

(7) 작성자인지 검사합니다.

(8) (3)에서 작성자는 임의로 1로 넣었으므로 isAuthor(1)은 참이어야 하고, isAuthor(-1)은 거짓이어야 합니다.

(9) 서비스를 이용해 데이터를 갱신합니다.

(10) 갱신 자체가 잘 되었는지(문법적 오류가 없었는지)와 실제로 데이터가 변경되었는지를 각각 $updateSuccess$updated_title 로 검사할 수 있습니다.

(11) 삭제는 굳이 글 정보를 사용할 필요가 없기 때문에 setup_post()를 사용하지 않고 직접 입력만 하는 insert_post() 메소드를 사용합니다.

(12) 데이터를 삭제해 봅니다.

(13) 없는 사용자 아이디일 경우 실패(not_deleted), 아니라면 성공(deleted)입니다.

(14) 목록은 데이터가 여러 개 필요하죠. 그래서 간단한 반복문을 이용해 데이터를 5개 집어넣습니다.

(15) 첫번째 페이지의 목록을 가지고 옵니다. 서비스에서 페이지당 글 갯수를 10개로 고정시켜 두었기 때문에 입력된 데이터 전부를 가지고 오는 것을 기대합니다.

(16) 페이저, 글 목록이 비어있지 않고, 의도한 대로 입력한 글 5개가 맞는지 확인합니다.

(17) (15)와는 다르게 두번째 페이지의 목록을 가지고 옵니다.

(18) 글이 5개밖에 없으므로 두번째 페이지는 0개여야 합니다.


테스트 실행도 잊지 말아요.

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

결과는 이렇게 나옵니다.

Post Tests (service\PostTests)
 ✔ 글생성 성공  134 ms
 ✔ 글생성 유효성검사  57 ms
 ✔ 글 조회  65 ms
 ✔ 작성자 확인  70 ms
 ✔ 수정  79 ms
 ✔ 삭제  77 ms
 ✔ 목록  97 ms
 ✔ 목록 2페이지  101 ms

Time: 00:00.748, Memory: 22.00 MB

OK (8 tests, 18 assertions)

컨트롤러 테스트하기

테스트가 깨졌는지 확인하기

컨트롤러 테스트를 추가하기 전에 한번 실행해 봅시다.

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

처음에는 성공했던 테스트가 실패했습니다!

Post Tests (controller\PostTests)
 ✘ 컨트롤러 글 생성 저장  96 ms
   ┐
   ├ Error: Cannot use object of type App\Entities\PostEntity as array
   ┴

ERRORS!
Tests: 3, Assertions: 6, Errors: 1.

메세지에서 힌트를 찾을 수 있습니다.

Error: Cannot use object of type App\Entities\PostEntity as array

우리가 엔티티에서 배열로 타입을 바꿈으로써 테스트 코드가 깨졌다고 나오네요.
컨트롤러 글 생성 저장 에서 실패했다고 나오므로 test_컨트롤러_글_생성_저장 메소드를 살펴봅니다. 배열 문법을 쓰는 곳이 한군데 있군요.
/tests/controller/PostTests.php

$this->assertStringContainsString("제목입니다", $created_post['title']);

객체 문법으로 바꿉시다.

$this->assertStringContainsString("제목입니다", $created_post->title);

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

php phpunit.phar --testdox --verbose  tests/controller/PostTests.php
Post Tests (controller\PostTests)
 ✔ 컨트롤러 글 생성 화면  145 ms
 ✔ 컨트롤러 글 생성 로그인페이지 이동  57 ms
 ✔ 컨트롤러 글 생성 저장  76 ms

Time: 00:00.345, Memory: 22.00 MB

OK (3 tests, 7 assertions)

성공했습니다. 이제 다른 테스트 케이스를 만들 수 있게 되었습니다.

컨트롤러 테스트 코드 추가하기

컨트롤러 테스트도 케이스에 따라 추가해 보겠습니다.

컨트롤러는 서비스 레이어를 사용하므로, 서비스에 대한 참조를 추가합니다.
/tests/controller/PostTests.php

public function setUp(): void // (2)
{
    parent::setUp();
    $this->postService = PostService::factory(); // (2)
}

테스트를 작성합니다.
/tests/controller/PostTests.php

private function insert_post() // (1)
{
    $post_data = [
        'title' => '제목입니다.',
        'content' => '본문은 10글자 이상이죠?'
    ];

    $memberId = 1;

    $postService = PostService::factory(); // (2)
    list($result, $post_id, $errors) = $postService->create($post_data, $memberId);
    return [$result, $post_id, $errors];
}

private function setup_post() // (3)
{
    list($result, $post_id, $errors) = $this->insert_post();

    $postService = PostService::factory();
    return $postService->find($post_id);
}

public function test_조회() // (4)
{
    // given
    list($result, $post_id, $errors) = $this->insert_post();

    // when
    $result = $this->get("/post/show/$post_id");

    // then
    $result->assertOK();
    $result->assertSee("제목입니다.");
    $result->assertDontSee("수정");
}

public function test_조회_로그인() // (5)
{
    // given
    list($result, $post_id, $errors) = $this->insert_post();
    $session_data = ['member_id' => 1];

    // when
    $result = $this->withSession($session_data)->get("/post/show/$post_id");

    // then
    $result->assertOK();
    $result->assertSee("제목입니다.");
    $result->assertSee("수정");
}

public function test_수정_로그인_안함() // (6)
{
    // given
    list($result, $post_id, $errors) = $this->insert_post();

    // when
    $result = $this->get("/post/edit/$post_id");

    // then
    $result->assertOK();
    $result->assertStatus(302);
    $redirectUrl = $result->response->getHeaderLine('Location');
    $result->assertStringContainsString("/post", $redirectUrl);
}

public function test_수정_로그인_GET() // (7)
{
    // given
    list($result, $post_id, $errors) = $this->insert_post();
    $session_data = ['member_id' => 1];

    // when
    $result = $this->withSession($session_data)->get("/post/edit/$post_id");

    // then
    $result->assertOK();
    $result->assertSee("제목입니다.");
}

public function test_수정_로그인_POST() // (8)
{
    // given
    list($result, $post_id, $errors) = $this->insert_post();
    $session_data = ['member_id' => 1];

    $post_data = [
        'title' => '새로운 제목입니다.',
        'content' => '새로운 본문입니다. 변경될까요?'
    ];

    // when
    $result = $this->withSession($session_data)->post("/post/edit/$post_id", $post_data);

    // then
    $result->assertOK();
    $result->assertStatus(302);
    $redirectUrl = $result->response->getHeaderLine('Location');
    $this->assertEquals("/post/show/$post_id", $redirectUrl);


    $postModel = new PostsModel();
    $updated_post = $postModel->find($post_id);
    $this->assertEquals($updated_post->title, $post_data['title']);
}

public function test_삭제_HTTP_메소드_GET() // (9)
{
    // given
    list($result, $post_id, $errors) = $this->insert_post();
    $session_data = ['member_id' => 1];

    // when
    $result = $this->withSession($session_data)->get("/post/delete");

    // then
    $result->assertStatus(302);
}

public function test_삭제_HTTP_메소드_POST_로그인_안함() // (10)
{
    // given
    list($result, $post_id, $errors) = $this->insert_post();

    // when
    $result = $this->post("/post/delete");

    // then
    $result->assertStatus(302);
}

public function test_삭제_HTTP_메소드_POST_로그인()// (11)
{
    // given
    list($result, $post_id, $errors) = $this->insert_post();
    $session_data = ['member_id' => 1];
    $post_data = ["post_id" => $post_id];

    // when
    $result = $this->withSession($session_data)->post("/post/delete", $post_data);

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

    $postModel = new PostsModel();
    $deleted_post = $postModel->find($post_id);
    $this->assertNull($deleted_post);
}

public function test_목록() // (12)
{
    // given
    foreach (range(1, 5) as $item) {
        $this->insert_post();
    }

    // when
    $result = $this->get("/post/index"); // (13)

    // then
    $result->assertOK();
    $result->assertSee("제목입니다.");

    //fwrite(STDERR, print_r($result, TRUE));
}

(1) 서비스 테스트와 마찬가지로 데이터를 준비하는 역할을 합니다.

(2) 서비스에서 멤버 변수로 $postService 인스턴스를 미리 만들어 두었던 것에 반해 컨트롤러 테스트에서는 직접 메소드에서 인스턴스를 만듭니다. 이유는 서비스와 달리 데이터를 준비하는 역할만 할 뿐 모든 테스트 메소드에서 직접 데이터베이스와 연동하는 것이 아니기 때문입니다.

(3) 서비스 테스트와 마찬가지로 데이터를 준비하고, 사용할 수 있게 PostEntity 객체를 리턴합니다.

(4) 로그인하지 않은 상태로 조회하는 테스트 코드입니다. "수정"이라는 글자가 보이지 않아야 합니다.

(5) 로그인된 상태로 조회하는 테스트 코드입니다. "수정"이라는 글자가 보여야 합니다.

(6) 수정 페이지에 로그인하지 않은 상태로 접근하면 목록 페이지로 리다이렉트하는 것을 테스트합니다.

(7) 수정 페이지에서 로그인한 상태로 HTTP GET 메소드를 보냈을 경우 수정 폼이 나오는 것을 확인합니다.

(8) 수정 페이지에서 로그인한 상태로 HTTP POST 메소드를 보냈을 경우 데이터가 수정되는 것을 확인합니다.

(9) 삭제 페이지에서 HTTP GET 메소드로 보냈을 경우 목록 페이지로 리다이렉트하는 것을 테스트합니다.

(10) 삭제 페이지에서 로그인하지 않은 상태로 HTTP POST 메소드로 보냈을 경우 목록 페이지로 리다이렉트하는 것을 테스트합니다.

(11) 삭제 페이지에서 로그인한 상태로 HTTP POST 메소드로 보냈을 경우 데이터가 수정되는 것을 테스트합니다.

(12) 목록 페이지를 테스트합니다.

(13) 목록 페이지의 경우 URL은 /post 지만 테스트시에는 /post/index 로 접근해야 함에 유의합니다.

최종 테스트 결과 확인하기

대망의 최종 테스트 시간입니다.

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

결과가 좋습니다. 모두 초록불이 들어왔습니다.

Post Tests (controller\PostTests)
 ✔ 컨트롤러 글 생성 화면  51 ms
 ✔ 컨트롤러 글 생성 로그인페이지 이동  7 ms
 ✔ 컨트롤러 글 생성 저장  16 ms
 ✔ 조회  12 ms
 ✔ 조회 로그인  12 ms
 ✔ 수정 로그인 안함  9 ms
 ✔ 수정 로그인 GET  10 ms
 ✔ 수정 로그인 POST  9 ms
 ✔ 삭제 HTTP 메소드 GET  9 ms
 ✔ 삭제 HTTP 메소드 POST 로그인 안함  8 ms
 ✔ 삭제 HTTP 메소드 POST 로그인  9 ms
 ✔ 목록  14 ms

Time: 00:00.197, Memory: 26.00 MB

좋은 웹페이지 즐겨찾기