테스트

CodeIgniter는 프레임워크와 애플리케이션 모두를 최대한 쉽게 테스트할 수 있도록 설계되었습니다. PHPUnit에 대한 지원이 내장되어 있으며, 애플리케이션의 모든 측면을 가능한 한 쉽게 테스트할 수 있도록 다양한 편의 헬퍼 메서드를 제공합니다.

시스템 설정

PHPUnit 설치

CodeIgniter는 모든 테스트의 기반으로 PHPUnit을 사용합니다. 시스템에서 PHPUnit을 설치하는 방법은 두 가지가 있습니다.

Composer

권장하는 방법은 Composer를 사용하여 프로젝트에 설치하는 것입니다. 전역 설치도 가능하지만, 시간이 지남에 따라 시스템의 다른 프로젝트와 호환성 문제가 발생할 수 있으므로 권장하지 않습니다.

시스템에 Composer가 설치되어 있는지 확인하십시오. 프로젝트 루트(애플리케이션 및 시스템 디렉토리가 포함된 디렉토리)에서 커맨드 라인에 다음을 입력하십시오:

composer require --dev phpunit/phpunit

이렇게 하면 현재 PHP 버전에 맞는 올바른 버전이 설치됩니다. 설치가 완료되면 다음을 입력하여 이 프로젝트의 모든 테스트를 실행할 수 있습니다:

vendor/bin/phpunit

Windows를 사용하는 경우 다음 명령을 사용하십시오:

vendor\bin\phpunit

Phar

다른 옵션은 PHPUnit 사이트에서 .phar 파일을 다운로드하는 것입니다. 이 파일은 프로젝트 루트에 배치해야 하는 독립 실행형 파일입니다.

애플리케이션 테스트

PHPUnit 설정

CodeIgniter 프로젝트 루트에는 phpunit.dist.xml 파일이 있습니다. 이 파일은 애플리케이션의 단위 테스트를 제어합니다. 직접 phpunit.xml을 제공하면 이 파일을 재정의합니다.

기본적으로 테스트 파일은 프로젝트 루트의 tests 디렉토리 아래에 배치됩니다.

테스트 클래스

제공되는 추가 도구를 활용하려면 테스트가 CodeIgniter\Test\CIUnitTestCase를 상속해야 합니다.

테스트 파일 배치에 대한 규칙은 없습니다. 그러나 테스트 파일의 위치를 빠르게 파악할 수 있도록 미리 배치 규칙을 정해두는 것을 권장합니다.

이 문서에서는 app 디렉토리의 클래스에 해당하는 테스트 파일을 tests/app 디렉토리에 배치합니다. 새 라이브러리 app/Libraries/Foo.php를 테스트하려면 tests/app/Libraries/FooTest.php에 새 파일을 생성합니다:

<?php

namespace App\Libraries;

use CodeIgniter\Test\CIUnitTestCase;

class FooTest extends CIUnitTestCase
{
    public function testFooNotBar()
    {
        // ...
    }
}

모델 app/Models/UserModel.php를 테스트하려면 tests/app/Models/UserModelTest.php에 다음과 같이 작성하게 됩니다:

<?php

namespace App\Models;

use CodeIgniter\Test\CIUnitTestCase;

class UserModelTest extends CIUnitTestCase
{
    public function testFooNotBar()
    {
        // ...
    }
}

테스트 스타일이나 필요에 맞는 디렉토리 구조를 자유롭게 생성할 수 있습니다. 테스트 클래스에 네임스페이스를 지정할 때, app 디렉토리가 App 네임스페이스의 루트임을 기억하십시오. 따라서 사용하는 모든 클래스는 App에 상대적인 올바른 네임스페이스를 가져야 합니다.

참고

테스트 클래스에 네임스페이스가 반드시 필요한 것은 아니지만, 클래스 이름 충돌을 방지하는 데 도움이 됩니다.

데이터베이스 결과를 테스트할 때는 클래스에서 DatabaseTestTrait을 사용해야 합니다.

준비 단계

대부분의 테스트는 올바르게 실행되기 위해 어느 정도의 준비가 필요합니다. PHPUnit의 TestCase는 준비 및 정리를 위한 네 가지 메서드를 제공합니다:

public static function setUpBeforeClass(): void
public static function tearDownAfterClass(): void

protected function setUp(): void
protected function tearDown(): void

정적 메서드 setUpBeforeClass()tearDownAfterClass()는 전체 테스트 케이스 전후에 실행되는 반면, 보호된 메서드 setUp()tearDown()은 각 테스트 사이에 실행됩니다.

이러한 특수 메서드 중 하나를 구현하는 경우, 확장된 테스트 케이스가 준비 단계를 방해하지 않도록 반드시 부모 메서드도 호출해야 합니다:

<?php

namespace App\Models;

use CodeIgniter\Test\CIUnitTestCase;

final class UserModelTest extends CIUnitTestCase
{
    protected function setUp(): void
    {
        parent::setUp(); // Do not forget

        helper('text');
    }

    // ...
}

트레이트

테스트를 향상시키는 일반적인 방법은 트레이트를 사용하여 여러 테스트 케이스에 걸쳐 준비 단계를 통합하는 것입니다. CIUnitTestCase는 클래스 트레이트를 감지하고 트레이트 이름에 따라 명명된 준비 메서드를 찾아 실행합니다 (예: setUp{NameOfTrait}()tearDown{NameOfTrait}()).

예를 들어, 일부 테스트 케이스에 인증을 추가해야 하는 경우 로그인한 사용자를 가장하는 설정 메서드가 있는 인증 트레이트를 생성할 수 있습니다:

<?php

namespace App\Traits;

trait AuthTrait
{
    protected function setUpAuthTrait()
    {
        $user = $this->createFakeUser();
        $this->logInUser($user);
    }

    // ...
}
<?php

namespace Tests;

use App\Traits\AuthTrait;
use CodeIgniter\Test\CIUnitTestCase;

final class AuthenticationFeatureTest extends CIUnitTestCase
{
    use AuthTrait;

    // ...
}

추가 어설션

CIUnitTestCase는 유용하게 활용할 수 있는 추가적인 단위 테스트 어설션을 제공합니다.

assertLogged($level, $expectedMessage)

예상했던 내용이 실제로 로그에 기록되었는지 확인합니다:

assertLogContains($level, $logMessage)

로그에 메시지 일부를 포함하는 레코드가 있는지 확인합니다.

<?php

$config = new \Config\Logger();
$logger = new \CodeIgniter\Log\Logger($config);

// check verbatim the log message
$logger->log('error', "That's no moon");
$this->assertLogged('error', "That's no moon");

// check that a portion of the message is found in the logs
$exception = new \RuntimeException('Hello world.');
$logger->log('error', $exception->getTraceAsString());
$this->assertLogContains('error', '{main}');
assertEventTriggered($eventName)

예상했던 이벤트가 실제로 트리거되었는지 확인합니다:

<?php

use CodeIgniter\Events\Events;

Events::on('foo', static function ($arg) use (&$result) {
    $result = $arg;
});

Events::trigger('foo', 'bar');

$this->assertEventTriggered('foo');
assertHeaderEmitted($header, $ignoreCase = false)

헤더 또는 쿠키가 실제로 전송되었는지 확인합니다:

<?php

$response->setCookie('foo', 'bar');

ob_start();
$this->response->send();
$output = ob_get_clean(); // in case you want to check the actual body

$this->assertHeaderEmitted('Set-Cookie: foo=bar');

참고

이를 사용하는 테스트 케이스는 PHPUnit에서 별도의 프로세스로 실행해야 합니다 (@runInSeparateProcess annotation 또는 RunInSeparateProcess attribute 사용).

assertHeaderNotEmitted($header, $ignoreCase = false)

헤더 또는 쿠키가 전송되지 않았는지 확인합니다:

<?php

$response->setCookie('foo', 'bar');

ob_start();
$this->response->send();
$output = ob_get_clean(); // in case you want to check the actual body

$this->assertHeaderNotEmitted('Set-Cookie: banana');

참고

이를 사용하는 테스트 케이스는 PHPUnit에서 별도의 프로세스로 실행해야 합니다 (@runInSeparateProcess annotation 또는 RunInSeparateProcess attribute 사용).

assertCloseEnough($expected, $actual, $message = ‘’, $tolerance = 1)

확장된 실행 시간 테스트를 위해, 예상 시간과 실제 시간의 절대 차이가 지정된 허용 범위 내에 있는지 테스트합니다:

<?php

use CodeIgniter\Debug\Timer;

$timer = new Timer();
$timer->start('longjohn', strtotime('-11 minutes'));
$this->assertCloseEnough(11 * 60, $timer->getElapsedTime('longjohn'));

위 테스트는 실제 시간이 660초 또는 661초인 경우를 허용합니다.

assertCloseEnoughString($expected, $actual, $message = ‘’, $tolerance = 1)

확장된 실행 시간 테스트를 위해, 문자열로 형식화된 예상 시간과 실제 시간의 절대 차이가 지정된 허용 범위 내에 있는지 테스트합니다:

<?php

use CodeIgniter\Debug\Timer;

$timer = new Timer();
$timer->start('longjohn', strtotime('-11 minutes'));
$this->assertCloseEnoughString(11 * 60, $timer->getElapsedTime('longjohn'));

위 테스트는 실제 시간이 660초 또는 661초인 경우를 허용합니다.

보호된/비공개 속성 접근

테스트 시, 다음의 세터 및 게터 메서드를 사용하여 테스트 중인 클래스의 보호된 메서드와 비공개 메서드 및 속성에 접근할 수 있습니다.

getPrivateMethodInvoker($instance, $method)

클래스 외부에서 비공개 메서드를 호출할 수 있게 합니다. 호출 가능한 함수를 반환합니다. 첫 번째 매개변수는 테스트할 클래스의 인스턴스이고, 두 번째 매개변수는 호출하려는 메서드의 이름입니다.

<?php

use App\Libraries\Foo;

// Create an instance of the class to test
$obj = new Foo();

// Get the invoker for the 'privateMethod' method.
$method = self::getPrivateMethodInvoker($obj, 'privateMethod');

// Test the results
$this->assertEquals('bar', $method('param1', 'param2'));
getPrivateProperty($instance, $property)

클래스 인스턴스에서 비공개/보호된 클래스 속성의 값을 가져옵니다. 첫 번째 매개변수는 테스트할 클래스의 인스턴스이고, 두 번째 매개변수는 속성의 이름입니다.

<?php

use App\Libraries\Foo;

// Create an instance of the class to test
$obj = new Foo();

// Test the value
$this->assertEquals('bar', $this->getPrivateProperty($obj, 'baz'));
setPrivateProperty($instance, $property, $value)

클래스 인스턴스 내에서 보호된 값을 설정합니다. 첫 번째 매개변수는 테스트할 클래스의 인스턴스이고, 두 번째 매개변수는 값을 설정할 속성의 이름이며, 세 번째 매개변수는 설정할 값입니다:

<?php

use App\Libraries\Foo;

// Create an instance of the class to test
$obj = new Foo();

// Set the value
$this->setPrivateProperty($obj, 'baz', 'oops!');

// Do normal testing...

서비스 모킹

테스트를 관련 코드만으로 제한하면서 서비스로부터 다양한 응답을 시뮬레이션하기 위해 app/Config/Services.php에 정의된 서비스 중 하나를 모킹해야 하는 경우가 자주 있습니다. 이는 컨트롤러 테스트 및 기타 통합 테스트 시 특히 그렇습니다. Services 클래스는 이를 간소화하기 위한 다음 메서드들을 제공합니다.

Services::injectMock()

이 메서드를 사용하면 Services 클래스에서 반환될 정확한 인스턴스를 정의할 수 있습니다. 서비스가 특정 방식으로 동작하도록 속성을 설정하거나, 서비스를 모의 클래스로 대체하는 데 사용할 수 있습니다.

<?php

namespace Tests;

use CodeIgniter\HTTP\CURLRequest;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Services;

final class SomeTest extends CIUnitTestCase
{
    public function testSomething()
    {
        $curlrequest = $this->getMockBuilder(CURLRequest::class)
            ->onlyMethods(['request'])
            ->getMock();
        Services::injectMock('curlrequest', $curlrequest);

        // Do normal testing here....
    }
}

첫 번째 매개변수는 교체할 서비스입니다. 이름은 Services 클래스의 함수 이름과 정확히 일치해야 합니다. 두 번째 매개변수는 교체할 인스턴스입니다.

Services::reset()

Services 클래스에서 모든 모의 클래스를 제거하여 원래 상태로 되돌립니다.

CIUnitTestCase가 제공하는 $this->resetServices() 메서드를 사용할 수도 있습니다.

참고

이 메서드는 Services의 모든 상태를 초기화하며, RouteCollection에 라우트가 없게 됩니다. 라우트를 불러오려면 Services::routes()->loadRoutes()와 같이 loadRoutes() 메서드를 호출해야 합니다.

Services::resetSingle(string $name)

이름으로 단일 서비스의 모든 모의 및 공유 인스턴스를 제거합니다.

참고

Cache, Email, Session 서비스는 침투적인 테스트 동작을 방지하기 위해 기본적으로 모킹됩니다. 모킹을 방지하려면 클래스 속성에서 해당 메서드 콜백을 제거하십시오: $setUpMethods = ['mockEmail', 'mockSession'];

팩토리 인스턴스 모킹

Services와 마찬가지로, 테스트 중에 Factories와 함께 사용할 사전 구성된 클래스 인스턴스를 제공해야 하는 경우가 있습니다. Services와 동일한 Factories::injectMock()Factories::reset() 정적 메서드를 사용하지만, 컴포넌트 이름을 위한 추가 선행 매개변수가 필요합니다:

<?php

namespace Tests;

use App\Models\UserModel;
use CodeIgniter\Config\Factories;
use CodeIgniter\Test\CIUnitTestCase;
use Tests\Support\Mock\MockUserModel;

final class SomeTest extends CIUnitTestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        $model = new MockUserModel();
        Factories::injectMock('models', UserModel::class, $model);
    }
}

참고

모든 컴포넌트 팩토리는 기본적으로 각 테스트 사이에 초기화됩니다. 인스턴스를 유지해야 하는 경우 테스트 케이스의 $setUpMethods를 수정하십시오.

테스트와 시간

시간에 의존하는 코드를 테스트하는 것은 어려울 수 있습니다. 그러나 Time 클래스를 사용하면 테스트 중에 현재 시간을 자유롭게 고정하거나 변경할 수 있습니다.

다음은 현재 시간을 고정하는 샘플 테스트 코드입니다:

<?php

namespace Tests;

use CodeIgniter\I18n\Time;
use CodeIgniter\Test\CIUnitTestCase;

final class TimeDependentCodeTest extends CIUnitTestCase
{
    protected function tearDown(): void
    {
        parent::tearDown();

        // Reset the current time.
        Time::setTestNow();
    }

    public function testFixTime(): void
    {
        // Fix the current time to "2023-11-25 12:00:00".
        Time::setTestNow('2023-11-25 12:00:00');

        // This assertion always passes.
        $this->assertSame('2023-11-25 12:00:00', (string) Time::now());
    }
}

Time::setTestNow() 메서드를 사용하여 현재 시간을 고정할 수 있습니다. 선택적으로 두 번째 매개변수에 로케일을 지정할 수 있습니다.

테스트 후에는 매개변수 없이 호출하여 현재 시간을 반드시 초기화하는 것을 잊지 마십시오.