[PHP] PHPUnit으로 단위 테스트(Unit Test)하기 프로그래밍

PHP로 자그마한 일을 하나 하게 되었다.
현재 내가 있는 회사는 단위 테스트(Unit Test)를 상당히 철저하게 하는 편이다.

그리하여 이번에 내가 하는 프로젝트도 단위 테스트를 하기로 했다.

또한, 단위 테스트를 제대로 하려면 프로그램을 짤때 각 기능을 함수로 잘 분리하거나 아니면 아예 객체지향적으로 짜야만 한다. 그렇지 않으면 단위테스트가 매우 까다롭다. 그래서 모든 비즈니스 로직은 클래스에 구현하고 자체적인 MVC 패턴 프레임워크도 만들었다. 거의 Java에서 MVC로 개발할 때와 유사한 방식으로 했다.

Unit Test Framework

PHP용 단위 테스트 프레임워크는 크게 두 죵류가 있는 것으로 보인다.
* PHPUnit
* SimpleTest for PHP

이 중에서 나는 PHPUnit을 선택하였다. 이유는, 이래저래 조사해 보니 PHPUnit이 더 많이 쓰이는 것 같고 상당히 오랜 기간 동안 꾸준히 개발되고 있으며, 오라일리 출판사에서 책도 나온 상태이고 Code Coverage 까지도 제공해 주기 때문이다.

SimpleTest 같은 경우도 잘 만들어진 것 같긴 하다. Eclipse용 플러그인도 제공되고, HTTP 요청을 시뮬레이션 할 수 있는 테스팅도 가능하다. 하지만 Code Coverage를 공식적으로 지원하지는 않고 무엇보다 책(잘 정리된 문서)이 없기 때문에 그냥 넘겼다.

PHPUnit 설치

$ pear channel-discover pear.phpunit.de
$ pear install phpunit/PHPUnit

php.ini 의 include_path에 PEAR경로가 포함되어 있는지 확인한다.

테스트 코드 작성

<?php
require_once 'PHPUnit/Framework.php';

class ArrayTest extends PHPUnit_Framework_TestCase
{
    public function testBlahBlah()
    {
        // Test 수행
    }

}
?>

PHPUnit 2.x 에서는 PHPUnit2_ 로 시작하는 클래스를 상속했으나, 지금은(3.x) 그냥 PHPUnit_ 으로 시작하는 클래스를 상속받는다.

테스트 실행

$ phpunit UnitTest [UnitTest.php]

ArrayTest는 테스트클래스의 이름이며, 그 뒤의 해당 클래스가 정의된 PHP 파일 이름 지정을 생략하면 ArrayTest.php 파일에 클래스가 정의되어 있다고 가정한다.

Unit Test 클래스는 PHPUnit_Framework_TestCase 를 상속받거나, public static suite() 메소드에서 PHPUnit_Framework_Test 클래스의 객체를 리턴해줘야 한다.

설정 코드(Fixture)

설정 코드(Fixture)란, 테스트를 수행할 때, 테스트 수행전에 미리 실행되어 있어야할 기본 설정 코드를 의미한다.

각 테스트 메소드(test로 시작하는 메소드들)을 수행하기 전에 항상 setUp() 메소드가 수행된다. 여기에 설정 코드를 넣으면 된다.
protected function setUp() {
    ...
}

또한, 한개의 테스트 메소드가 끝날 때마다 항상 tearDown() 메소드가 호출된다.
protected function tearDown() {
    ...
}

tearDown()은 명시적으로 해제해야할 자원이 있을 때만 수행하면 된다.

PHPUnit_Extensions_TestSetup 을 이용해서 Suite 레벨의 설정 코드를 만들 수 있다.

Test Suite 테스트 묶음

PHPUnit_Framework_TestSuite 클래스를 통해서 테스트 묶음을 만들 수 있다.
<?php
require_once "PHPUnit/Framework.php";
require_once "PHPUnit/TextUI/TestRunner.php";
require_once "ArrayTest.php";

class AllTests
{
    public static function suite()
    {
        $suite = new PHPUnit_Framework_TestSuite("PHPUnit Frameowrk");

        // 테스트할 테스트 클래스를 추가한다.
        $suite->addTestSuite("ArrayTest");

        //... 계속 추가한다....
        return $suite;
    }
}


"$ phpunit AllTests" 로 실행하면, suite() 메소드에서 추가한 테스트 클래스들이 실행된다.

TestCase의 확장

PHPUnit_Framework_TestCase를 확장해서 만들어진 테스트 케이스들이 있다. 자세한 사항은 TestCase Extensions를 참조한다.
require_once "PHPUnit/Extensions/확장클래스.php";

 * PHPUnit_Extensions_ExceptionTestCase 상속
  : 특정 예외가 발생하는지 테스트
  : $this->setExpectedException('Exception') 사용

 * PHPUnit_Extensions_OutputTestCase 상속
  : 특정 출력(echo, print)가 나오는지 여부를 테스트한다.
public function testExpectFooActualFoo()
{
    $this->expectOutputString('foo');
    print 'foo';
}

  : void expectOutputRegex(string $regularExpression) 도 가능하다.

 * PHPUnit_Extensions_PerformanceTestCase 상속
  : 성능을 측정하는 테스트 케이스
  : setMaxRunningTime(최대실행시간-초단위); 로 설정

public function testPerformance()
{
    $this->setMaxRunningTime(2);
    sleep(1);
    // sleep(3)으로 바꾸면 테스트가 실패한다.
}


완성되지 않은 테스트

테스트를 작성하는 과정에서는 아직 완성되지 않은 테스트를 포함하고 있는 경우도 있다. 그럴 때, 이 테스트가 아직 미완성임을 표시하는 방법이 있다.
<?php
require_once "PHPUnit/Framework.php";

class SampleTest extends PHPUnit_Framework_TestCase
{
    public function testSomething()
    {
        // Optional: 일단 구현한 테스트...
        $this->assertTrue(TRUE, "그냥 쓸데 없는 테스트..");

        $this->markTestIncomplete("이 테스트는 아직 완전히 구현되지 않았습니다.");
    }
}
?>

위를 실행하면, "I" 마크가 나오고, OK, but incomplete or skipped tests! Tests: 1, Incomplete: 1. 라는 메시지도 나온다.
void markTestIncomplete();
void markTestIncomplete(string $message)


테스트 건너 뛰기

테스트메소드에서 다음 메소드를 수행하면 테스트를 건너 뛴다. "S" 마크가 나오고 OK, but incomplete or skipped tests!  Tests: 1, Skipped: 1. 와 같은 메시지가 출력된다.
void markTestSkipped();
void markTestSkipped(string $message);


Mock Objects(모의 객체)

모의 객체 생성하고 사용해보자. 모의 객체는 이미 존재하는 클래스의 역할을 대신 해주는 가짜 객체이다.

모의 객체 생성시, 모의로 생성할 클래스의 생성자에 인자가 필요하다면, 아래의 array("param1") 부분에서 처럼 생성자의 파라미터를 배열로 만들어 넘기면 된다. 생성자의 파라미터를 넘기지 않아도 경고만 발생할 뿐 실제 모의 객체 사용에 문제는 없었다.


$mockObject = $this->getMock('ClassName', array('method1', 'method2'), array("param1"));

아래는 모의 객체의 특정 메소드가 특정 값을 리턴하게 만드는 것이다.
$stub = $this->getMock('SomeClass');
$stub->expects($this->any()) # 이 메소드의 호출 회수를 지정한다.
     ->method('doSomething') # doSomething 메소드를 호출할 것이다.
     ->will($this->returnValue('foo')); # doSomething 메소드는 'foo'문자열을 리턴해줄 것이다.
# $stub 모의 객체를 사용한다.

자세한 사항은 Mock Objects 를 참조한다.

Code Coverage

XDebug가 깔려 있어야만 한다.
XDebug를 설치하기위해, XDebug 파일을 다운로드 하여, 기존 zend_extension_ts 을 주석처리하고 다음으로 대체한다. 패스는 알아서 맞춘다.
zend_extension_ts="E:/AutoSet/Server/php5/modules/php_xdebug-2.0.0rc2-5.1.2.dll"

XDebug가 아직은 불안정하여, 디버깅시 문제를 일으킨다. 만약 XDebug를 깐 상태에서 단위 테스트나 일반 PHP 어플리케이션이 의도한대로 움직이지 않는다면 XDebug를 제거하라.

$ phpunit --report 디렉토리명 ClassTest

위와 같이 --report 디렉토리명 옵션을 주면 해당 디렉토리에 테스트 결과의 코드 커버리지 정보를 HTML파일로 생성해준다.

전체 테스트하기

테스트 케이스 하나 하나가 아니라, 전체 테스트 케이스들을 한꺼번에 테스트하려면 suite 를 구성하면된다.
<?php

require_once 'MyClass1Test.php';
require_once 'MyClass2Test.php';

class AllTest
{
    public static function suite()
    {
        $suite = new PHPUnit_Framework_TestSuite('All Test');
        $suite->addTestSuite('MyClass1Test');
        $suite->addTestSuite('MyClass2Test');

        # 쭉쭉, 원하는 테스트 케이스 클래스를 작성해주면된다.
        $suite->addTestSuite('....');

        return $suite;
    }
}
?>

그리고는
$ phpunit AllTest

물론
$ phpunit --report reportall AllTest

위와 같이 실행하면 전체 코드에 대한 코드 커버리지를 확인할 수 있다.

핑백

덧글

  • 낭망백수 2007/01/24 14:02 # 삭제

    피쿨에서보고 질문드립니다.
    phpunit 도 eclipse plug-in 이나 혹은 웹프레임웍에 장착된 사례가 있나요?
    꾸벅~!

    ps; 가능하시다면 피쿨에도 답변남겨주시면 좋을 것 같습니다.
※ 로그인 사용자만 덧글을 남길 수 있습니다.