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

단위테스트의 정의

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

코드이그나이터4는 사람이 직접 테스트하는 엔드 투 엔드 테스트(end-to-end test) 외에 테스트 코드를 작성해서 테스트를 자동화하는 인티그레이션 테스트와 단위 테스트도 지원합니다.

테스트 코드를 작성하면 기능이 개선될 때마다 직접 눌러보거나 눈으로 확인하는 것이 아닌, 테스트가 성공했는지를 시스템적으로 확인할 수 있으므로 더 정확하고, 더 빠르고 더 편리한 기능 추가가 가능합니다.
덧붙여서 새로 작성한 기능이 기존의 기능을 깨뜨리는지 여부도 단위테스트로 알 수 있으므로 예상치 못한 사이드 이펙트도 예방하는 효과도 있습니다.

우리가 엔티티와 서비스 레이어를 분리한 것은 그저 그것이 더 최신의 프로그래밍 아키텍쳐이기 때문은 아닙니다. 좀 더 직접적으로 말하자면, HTTP 레이어와 비즈니스 로직 레이어를 분리함으로써 단위테스트가 쉽게 하기 위함도 있습니다.
게다가 단위 테스트를 작성해 놓으면, 굳이 프로그램의 소스를 볼 필요 없이 각 기능별 예제를 얻는 셈이 됩니다. 어떻게 사용하는지에 대한 가이드가 되는 셈이죠.

테스트 코드는 PHP에서 사실상 표준으로 여겨지는 PHPUnit 형식으로 만들어지게 됩니다.
PHPUnit은 컴포저를 통한 설치 후 실행하는 방법과 직접 phar를 통한 방법으로 실행할 수 있습니다. 우리는 composer를 사용해서 PHPUnit을 사용하지 않고 간단하게 phar 파일을 통해 단위테스트를 해 보겠습니다.

단위 테스트 준비하기

phpunit.phar 파일 복사

https://phar.phpunit.de/phpunit.phar 파일을 다운로드한 후 프로젝트 루트(/) 디렉토리에 복사합니다.
PHPUNIT의 버전은 변경될 수 있으므로 phpunit-9.5.2.phar 파일을 phpunit.phar로 변경합니다.
최종 경로는 /phpunit.phar입니다. 같은 디렉토리에 composer.phar 파일과 app 디렉토리가 보이면 됩니다.

phpunit 설정하기

/phpunit.xml.dist 파일을 복사해서 /phpunit.xml로 파일명을 변경합니다.
PHP 서버의 경로를 설정하는 부분을 수정합니다. 기존 경로는 아래와 같습니다. 39번째 줄에서 찾을 수 있습니다.
phpunit.xml

<server name="app.baseURL" value="http://example.com"/>

변경할 코드는 아래와 같습니다. 혹시 hosts 파일을 설정해서 쓰시거나 포트를 변경해서 쓴다면 알맞은 이름으로 바꿔주시면 됩니다.

<server name="app.baseURL" value="http://localhost:8080"/>

테스트 데이터베이스를 확인하겠습니다. 예제에서는 SQLITE3를 사용합니다. SQLITE3를 사용하려면 phpunit.xml 파일에서 아래의 내용이 있는지 확인하고, 없다면 추가합니다.

<env name="database.tests.database" value=":memory:"/>
<env name="database.tests.DBDriver" value="SQLite3"/>

SQLITE3 는 파일 기반 데이터베이스로 특별한 설정도 필요없고 데이터베이스 서버가 필요하지도 않은 가벼운 데이터베이스입니다. 실서비스를 하는 것은 무리겠지만 테스트용으로는 적합합니다.
특히 설정에서 볼 수 있듯이 메모리에서만 작동시킬 수도 있기 때문에 테스트 코드 실행 후 바로 데이터베이스가 자동으로 지워져 편리한 점도 있습니다. 테스트 중에는 잘못된 데이터가 들어가는 일이 많은데, 프로그램 오류가 아니라 잘못된 데이터로 인한 오류 가능성을 방지하기 때문입니다.

SQLITE3 php.ini 에서 활성화하기

만약 phpunit에서 SQLITE3를 이용해서 테스트를 하기로 결정했다면, php가 SQLITE3를 인식할 수 있도록 php.ini를 수정해야 합니다. 아래의 코드를 php.ini에서 찾아봅니다. 참고로 php.ini는 php가 설치된 경로에 함께 있습니다.
php.ini

# extension=sqlite3

위처럼 #으로 시작한다면 주석처리되어 있다는 뜻입니다. 앞의 #을 지웁니다.

extension=sqlite3

마이그레이션 파일 옮기기

단위 테스트는 우리가 작성한 코드이그나이터4 어플리케이션과는 다른 "테스트" 환경에서 실행되므로, 테스트를 위해서 마이그레이션 파일이 필요합니다. 다행이도 우리는 이미 MVP에서 마크다운 기능을 붙일 때 마이그레이션을 사용했으므로 마이그레이션 파일을 그대로 복사하겠습니다.
app/Database/Migrations/날짜_Posts.php 파일과 같은 경로의 *_Member.phptests/_support/Database/Migrations/ 경로에 복사합니다.

단위 테스트 작성하기

글 생성 단위 테스트 작성하기

/tests/service/PostTests.php 파일을 만들고 아래와 같이 입력합니다.
/tests/service/PostTests.php

<?php

namespace service;

use App\Services\PostService;
use CodeIgniter\Test\CIDatabaseTestCase; // (1)

class PostTests extends CIDatabaseTestCase // (2)
{
    public function setUp() :void // (3)
    {
        parent::setUp(); // (4)
    }

    public function tearDown() :void // (5)
    {
        parent::tearDown(); // (6)
    }

    public function test_글생성_성공(){ // (7)
        // given // (8)
        $post_data = [ // (9)
            'title' => '제목입니다.',
            'content' => '본문은 10글자 이상이죠?'
        ];

        $memberId = 1; // (10)

        // when // (11)
        list($result, $post_id, $errors) = PostService::factory()->create($post_data, $memberId); // (12)

        // then // (13)
        $this->assertTrue($result); // (14)
        $this->assertNotNull($post_id); // (15)
        $this->assertCount(0, $errors); // (16)
    }
}

(1) CIDatabaseTestCase 클래스를 상속받아서 테스트를 진행합니다. 순수한 유닛 테스트는 CIUnitTestCase를 상속하면 되지만, 데이터베이스를 연동할 필요가 있을 때는 CIDatabaseTestCase를 사용하면 됩니다.

(2) 클래스 이름은 파일명과 동일해야 합니다. 또한 의무는 아니지만 알아보기 쉽도록 클래스 이름 뒤에는 Test를 붙입니다.
(3) setUp() 메소드는 테스트가 실행되기 전에 실행됩니다. 즉, 미리 준비해야 할 동작이 있다면 setUp() 메소드에서 호출하면 됩니다.

(4) 부모 클래스 CIDatabaseTestCasesetUp() 메소드를 호출해서 테스트 동작에 영향이 없도록 합니다.

(5) tearDown() 메소드는 테스트 메소드가 끝날 때 호출됩니다. 테스트 이후 정리해야 할 리소스가 있다면 tearDown에서 하면 됩니다.

(6) parent::tearDown() 또한 (5)와 동일한 이유로 테스트 동작에 영향이 없도록 합니다.

(7) 테스트할 메소드를 작성합니다. PHPUnit에서 테스트는 반드시 test로 시작하거나 test로 끝나야 합니다.
메소드 이름을 보면 test_글생성_성공임을 볼 수 있습니다. 보통 단위 테스트 메소드 이름은 한글로 지어서 어떤 테스트가 실패했는지 한번에 알아보기 쉽게 쓰는 경우가 많습니다.

(8) given은 BDD(Behavior Driven Development)에서 쓰는 용어입니다.
특정 기능을 테스트 하기 위해 동작에 필요한 값을 준비하는 과정을 given이라고 합니다. "주어진 값"이라는 뜻이겠죠?
BDD는 TDD(Test Driven Development)와는 다르게 함수의 동작을 테스트하는 것이 아니라, 논리의 흐름을 테스트합니다. 즉, 우리가 실제로 원하는 결과가 나오는지 테스트 케이스를 작성하는 것입니다.

(9) 글 데이터를 준비합니다. 우리는 서비스 레이어를 분리함으로써 HTTP 계층과 무관하게 글이 잘 써지는지 테스트를 작성할 수 있게 되었습니다.

(10) 사용자 아이디는 임의로 설정하겠습니다.

(11) when은 BDD에서 "실행"을 나타냅니다. 실제로 테스트하고자 하는 기능을 동작시킵니다.

(12) 글 생성 기능을 실행합니다. 팩토리 패턴을 이용해서 한번에 글 생성 메소드를 실행할 수 있습니다.

(13) then은 BDD에서 "검증"을 뜻합니다. 메소드가 실행되고 나면 결과가 정상인지 알고 싶기 때문입니다.

(14) assertTrue는 값이 참인지 검사합니다. assert는 단언문이라고 불리는데, assert를 통과해야 단위 테스트를 통과하는 것입니다.

(15) assertNotNull은 이름에서 유추할 수 있듯이 != null임을 검사합니다. 우리는 서비스에서 데이터 입력이 실패할 경우 [false, null, $postModel->errors()] 를 리턴했으므로 성공했으면 null이 아님을 기대합니다.

(16) assertCount는 배열의 숫자를 셉니다. 성공인 경우 빈 배열 []이므로 숫자가 0임을 기대합니다.

글 생성 단위 테스트 실행하기

터미널에서 프로젝트 루트 디렉토리로 이동합니다.

cd {프로젝트_루트_디렉토리}

유닛 테스트를 실행합니다.

php phpunit.phar --testdox --verbose  tests/service/PostTests.php
  • --testdox 옵션은 메소드 이름을 출력하기 위해 사용했습니다.
  • --verbose 옵션은 출력 결과를 정돈시켜줍니다.
  • tests/service/PostTests.php는 테스트 파일의 경로를 적어주면 됩니다.

단위 테스트의 결과는 아래와 같이 출력됩니다.

Post Tests (service\PostTests)
 ✔ 글생성 성공  148 ms

Time: 00:00.220, Memory: 22.00 MB

OK (1 test, 3 assertions)

OK (1 test, 3 assertions)는 1개의 테스트에 3개의 단언문을 모두 통과했다는 뜻입니다.


만약 tests 디렉토리 아래의 모든 테스트 파일을 테스트하고 싶다면 아래와 같이 합니다.

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

글 생성 유효성 검사 테스트 작성하기

글 생성 유효성 검사 단위 테스트도 작성합니다. PostTests.php에 이어서 작성하면 됩니다.

public function test_글생성_유효성검사()
{
    // given
    $post_data = [ // (1)
        'title' => '',
        'content' => ''
    ];

    $memberId = 1;

    // when
    list($result, $post_id, $errors) = PostService::factory()->create($post_data, $memberId);

    $this->assertFalse($result);
    $this->assertNull($post_id);

    fwrite(STDERR, print_r($errors, TRUE)); // (2)
}

(1) 제목과 본문을 일부러 빈 값으로 설정했습니다. 우리가 기대한 바에 따르면 모델은 유효성 검사를 하고 오류 메세지를 내보내야 합니다.

(2) PHPUnit에서는 기본적으로 출력을 콘솔에 할 수 없습니다. 굳이 해야 할 이유도 없죠. 유닛 테스트의 목적은 그럼에도 불구하고 값을 꼭 콘솔에서 봐야 한다면 fwrite(STDERR, print_r(변수, TRUE)); 형식으로 출력할 수 있습니다.


다시 한번 유닛테스트를 실행시켜 봅시다.

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

아래와 같이 나오면 성공입니다.

Post Tests (service\PostTests)
 ✔ 글생성 성공  47 ms
Array
(
    [title] => 제목이 필요합니다
    [content] => 본문이 필요합니다
)
 ✔ 글생성 유효성검사  46 ms

Time: 00:00.119, Memory: 24.00 MB

OK (2 tests, 5 assertions)

좋은 웹페이지 즐겨찾기