テスト

CodeIgniter は、フレームワークとアプリケーションの両方のテストを可能な限り簡単にするように構築されています。 PHPUnit のサポートが組み込まれており、フレームワークはアプリケーションのあらゆる側面を可能な限り痛みのないようにテストするための便利なヘルパーメソッドを多数提供します。

システム設定

PHPUnit のインストール

CodeIgniter は、すべてのテストの基礎として PHPUnit を使用します。システム内で PHPUnit を使用するには、2 つの方法があります。

Composer

推奨される方法は、Composer を使用してプロジェクトにインストールすることです。グローバルにインストールすることも可能ですが、時間の経過とともにシステム上の他のプロジェクトとの互換性の問題が発生する可能性があるため、お勧めしません。

システムに Composer がインストールされていることを確認してください。プロジェクトのルート (アプリケーションディレクトリとシステムディレクトリを含むディレクトリ) から、コマンドラインで次のように入力します。

composer require --dev phpunit/phpunit

これにより、現在の PHP バージョンに適したバージョンがインストールされます。完了したら、次のように入力して、このプロジェクトのすべてのテストを実行できます。

vendor/bin/phpunit

Windows を使用している場合は、次のコマンドを使用します。

vendor\bin\phpunit

Phar

もう 1 つのオプションは、PHPUnit サイトから .phar ファイルをダウンロードすることです。これは、プロジェクトルート内に配置する必要があるスタンドアロンファイルです。

アプリケーションのテスト

PHPUnit 設定

フレームワークには、プロジェクトルートに phpunit.xml.dist ファイルがあります。これは、フレームワーク自体の単体テストを制御します。独自の phpunit.xml を提供すると、これが上書きされます。

アプリケーションを単体テストする場合は、system フォルダー、および vendor または ThirdParty フォルダーを除外する必要があります。

テストクラス

提供されている追加のツールを利用するには、テストが CIUnitTestCase を拡張する必要があります。すべてのテストは、デフォルトで tests/app ディレクトリにあると想定されます。

新しいライブラリ Foo をテストするには、tests/app/Libraries/FooTest.php に新しいファイルを作成します。

<?php

namespace App\Libraries;

use CodeIgniter\Test\CIUnitTestCase;

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

モデルの 1 つをテストするには、tests/app/Models/OneOfMyModelsTest.php のようになります。

<?php

namespace App\Models;

use CodeIgniter\Test\CIUnitTestCase;

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

テストスタイル/ニーズに合ったディレクトリ構造を作成できます。テストクラスに名前空間を設定する場合、app ディレクトリが App 名前空間のルートであることを覚えておいてください。したがって、使用するクラスには、App に相対的な正しい名前空間が必要です。

名前空間はテストクラスには厳密には必要ありませんが、クラス名が競合しないようにするのに役立ちます。

データベースの結果をテストする場合は、クラスで DatabaseTestTrait を使用する必要があります。

ステージング

ほとんどのテストでは、正しく実行するために何らかの準備が必要です。 PHPUnit の TestCase は、ステージングとクリーンアップに役立つ 4 つのメソッドを提供します。

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 OneOfMyModelsTest 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 で別のプロセスとして実行する必要があります。

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 で別のプロセスとして実行する必要があります。

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)

クラスの外部からプライベートメソッドを呼び出せるようにします。これにより、呼び出し可能な関数が返されます。最初のパラメータはテスト対象のクラスのインスタンスです。2番目のパラメータは呼び出したいメソッドの名前です。

<?php

use App\Libraries\Foo;

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

// Get the invoker for the 'privateMethod' method.
$method = $this->getPrivateMethodInvoker($obj, 'privateMethod');

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

クラスのインスタンスから、プライベート/プロテクトされたクラスプロパティの値を取得します。最初のパラメータはテスト対象のクラスのインスタンスです。2番目のパラメータはプロパティの名前です。

<?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)

クラスインスタンス内のプロテクトされた値を設定します。最初のパラメータはテスト対象のクラスのインスタンスです。2番目のパラメータは値を設定するプロパティの名前です。3番目のパラメータは設定する値です。

<?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 で定義されたサービスの1つをモックする必要があることがよくあります。これは、コントローラーやその他の統合テストをテストする場合に特に当てはまります。Services クラスには、これを簡単にするための次のメソッドが用意されています。

Services::injectMock()

このメソッドを使用すると、Servicesクラスによって返される正確なインスタンスを定義できます。これを使用して、サービスが特定の方法で動作するようにプロパティを設定したり、サービスをモックされたクラスに置き換えたりできます。

<?php

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

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

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

最初のパラメータは、置き換えるサービスです。名前は、Servicesクラス内の関数名と正確に一致する必要があります。2番目のパラメータは、置き換えるインスタンスです。

Services::reset()

Servicesクラスからすべてのモックされたクラスを削除し、元の状態に戻します。

また、$this->resetServices() メソッドも使用できます。CIUnitTestCase が提供しています。

このメソッドは、Servicesのすべての状態をリセットし、RouteCollection にはルートがなくなります。ルートをロードして使用したい場合は、Services::routes()->loadRoutes() のように loadRoutes() メソッドを呼び出す必要があります。

Services::resetSingle(string $name)

名前で指定された単一のサービスのモックと共有インスタンスを削除します。

CacheEmailSession の各サービスは、侵入的なテスト動作を防ぐために、デフォルトでモックされます。これらのモックを停止するには、クラスプロパティからメソッドコールバックを削除します: $setUpMethods = ['mockEmail', 'mockSession'];

Factoryインスタンスのモック

Servicesと同様に、テスト中に、Factories で使用される事前構成されたクラスインスタンスを提供する必要がある場合があります。Services と同様に、Factories::injectMock() および Factories::reset() 静的メソッドを使用しますが、コンポーネント名の追加パラメータを前に取ります。

<?php

namespace Tests;

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', 'App\Models\UserModel', $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() メソッドで現在の時間を固定できます。オプションで、2番目のパラメータとしてロケールを指定できます。

パラメータなしで呼び出すことで、テスト後に現在の時間をリセットすることを忘れないでください。

CLI出力のテスト

StreamFilterTrait

バージョン 4.3.0 で新しく追加されました。

StreamFilterTrait は、これらのヘルパーメソッドの代替手段を提供します。

テストが難しいものをテストする必要がある場合があります。場合によっては、PHP自身のSTDOUTやSTDERRのようなストリームをキャプチャすると役立つことがあります。StreamFilterTrait は、選択したストリームからの出力をキャプチャするのに役立ちます。

メソッドの概要

  • StreamFilterTrait::getStreamFilterBuffer() バッファからキャプチャされたデータを取得します。

  • StreamFilterTrait::resetStreamFilterBuffer() キャプチャされたデータをリセットします。

テストケースの1つ内でのデモンストレーションの例

<?php

use CodeIgniter\CLI\CLI;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\StreamFilterTrait;

final class SomeTest extends CIUnitTestCase
{
    use StreamFilterTrait;

    public function testSomeOutput(): void
    {
        CLI::write('first.');

        $this->assertSame("\nfirst.\n", $this->getStreamFilterBuffer());

        $this->resetStreamFilterBuffer();

        CLI::write('second.');

        $this->assertSame("second.\n", $this->getStreamFilterBuffer());
    }
}

StreamFilterTrait には、自動的に呼び出されるコンフィギュレーターがあります。「テストトレイト」を参照してください。

テストで setUp() メソッドまたは tearDown() メソッドをオーバーライドする場合は、それぞれ parent::setUp() メソッドと parent::tearDown() メソッドを呼び出して、StreamFilterTrait を構成する必要があります。

CITestStreamFilter

手動/単独使用のための CITestStreamFilter

1つのテストでのみストリームをキャプチャする必要がある場合は、StreamFilterTraitトレイトを使用する代わりに、ストリームに手動でフィルターを追加できます。

メソッドの概要

  • CITestStreamFilter::registration() フィルター登録。

  • CITestStreamFilter::addOutputFilter() 出力ストリームへのフィルターの追加。

  • CITestStreamFilter::addErrorFilter() エラーストリームへのフィルターの追加。

  • CITestStreamFilter::removeOutputFilter() 出力ストリームからのフィルターの削除。

  • CITestStreamFilter::removeErrorFilter() エラーストリームからのフィルターの削除。

<?php

use CodeIgniter\CLI\CLI;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Filters\CITestStreamFilter;

final class SomeTest extends CIUnitTestCase
{
    public function testSomeOutput(): void
    {
        CITestStreamFilter::registration();
        CITestStreamFilter::addOutputFilter();

        CLI::write('first.');

        CITestStreamFilter::removeOutputFilter();
    }
}

CLI入力のテスト

PhpStreamWrapper

バージョン 4.3.0 で新しく追加されました。

PhpStreamWrapper は、CLI::prompt()CLI::wait()CLI::input() などのユーザー入力を必要とするメソッドのテストを作成する方法を提供します。

PhpStreamWrapperはストリームラッパークラスです。PHPのストリームラッパーを知らない場合は、PHPマニュアルの「streamWrapperクラス」を参照してください。

メソッドの概要

  • PhpStreamWrapper::register() php プロトコルに PhpStreamWrapper を登録します。

  • PhpStreamWrapper::restore() phpプロトコルラッパーをPHP組み込みラッパーに戻します。

  • PhpStreamWrapper::setContent() 入力データを設定します。

重要

PhpStreamWrapperは php://stdin のテストのみを目的としています。ただし、登録すると、php://stdoutphp://stderrphp://memoryなどの phpプロトコルストリームをすべて処理します。したがって、必要な場合にのみ PhpStreamWrapper を登録/登録解除することを強くお勧めします。それ以外の場合、登録中に他の組み込みphpストリームと干渉します。

テストケースの1つ内でのデモンストレーションの例

<?php

use CodeIgniter\CLI\CLI;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\PhpStreamWrapper;

final class SomeTest extends CIUnitTestCase
{
    public function testPrompt(): void
    {
        // Register the PhpStreamWrapper.
        PhpStreamWrapper::register();

        // Set the user input to 'red'. It will be provided as `php://stdin` output.
        $expected = 'red';
        PhpStreamWrapper::setContent($expected);

        $output = CLI::prompt('What is your favorite color?');

        $this->assertSame($expected, $output);

        // Restore php protocol wrapper.
        PhpStreamWrapper::restore();
    }
}