テスト

適切なテストを書くことはより良いソフトウェアを書くのに役立ちます。

全ての依存がインジェクトされ、横断的関心事がAOPで提供されるクリーンなBEAR.Sundayのアプリケーションはテストフレンドリーです。フレームワーク固有の密結合したベースクラスやヘルパーメソッドなしで高いカバレッジのテストを記述することができます。

テスト実行

vendor/bin/phpunitまたはcomposer testコマンドでphpmdphpcsと共にテストを実行します。他にもこのようなcomposerコマンドがあります。

composer coverge  // testカバレッジ
composer cs       // コーディングスタンダード検査
composer cs-fix   // コーディングスタンダード修復

リソース テストケース作成

全てがリソースのBEAR.Sundayではリソース操作がテストの基本です。

これはMyvendor\MyProjectアプリをhtml-appコンテキストで実行して、page://self/todoリソースにPOSTすれば201 (Created)が返ってくることをテストするコードです。

<?php

class TodoTest extends TestCase
{
    /**
     * @var \BEAR\Resource\ResourceInterface
     */
    private $resource;
    protected function setUp()
    {
        $this->resource = (new AppInjector('Myvendor\MyProject', 'html-app'))->getInstance(ResourceInterface::class);
    }
    public function testOnPost()
    {
        $page = $this->resource->post->uri('page://self/todo')(['title' => 'test']);
        /* @var $page ResourceObject */
        $this->assertSame(StatusCode::CREATED, $page->code);
    }
}
  • AppリソースでCRUDテストはApp/TodoTestを参考にしてください。
  • PageリソースのテストはPage/Indexを参考にしてください。

アプリケーション・インジェクター

AppInjector(アプリケーションインジェクター)はアプリケーションで利用するすべてのクラスのインスタンスを特定のコンテキストを指定して生成することができ、リソースオブジェクトやその依存を直接テストすることができます。

$injector = new AppInjector('MyVendor\MyProject', 'test-app'));

// リソースクライアント
$resource = $injector->getInstance(ResourceInterface::class);
$index = $resource->uri('page://self/index')();
/* @var $index Index */
$this->assertSame(StatusCode::OK, $page->code);
$todos = $page->body['todos'];

// リソースクラスを直接生成して直接コール
$user = $resource->newInstance('app://self/user');
// or
$user = $injector->getInstance(User::class);
$name = $index->onGet(1)->body['name']; // BEAR

// フォームのバリデーション検査
$form = $injector->getInstance(TodoForm::class);
$submit = ['name' => 'BEAR'];
$isValid = $this->form->apply($submit); // true

テストダブル

テストダブル (Test Double) とは、ソフトウェアテストでテスト対象が依存しているコンポーネントを置き換える代用品のことです。テストダブルは以下のパターンがあります。

  • スタブ (テスト対象にダミーデータを与える)
  • モック (下位モジュールを正しく利用しているかを実際のモジュールを用いないで検証)
  • フェイク (実際のオブジェクトに近い働きをするがより単純な実装を使う)
  • スパイ (実際のオブジェクトに対する入出力の記録を検証)

全ての依存がインジェクトされるBEAR.SundayではDIでテストダブルを実現することは容易ですが、テストダブルフレームワークRay.TestDoubleを使うとさらに便利になりスパイもできるようになります。

composerインストール

$ composer require ray/test-double 1.x-dev --dev

TestModuleなど作成してモジュールインストールします。

use Ray\Di\AbstractModule;
use Ray\TestDouble\TestDoubleModule;

class AppModule extends AbstractModule
{
    protected function configure()
    {
        $this->install(new TestDoubleModule);
    }
}

テストダブルの対象に@Fakeableとアノテートとします。

use Ray\TestDouble\Annotation\Fakeable;

/**
 * @Fakeable
 */
class Foo
{
    public function getDate() {
        return date("Ymd");
    }
}

テストダブルのクラスにはFakeプリフェックスをつけてtests/fake-srcフォルダに保存します。元クラスをextendして入れ替えるクラスだけを実装します。

class FakeFoo extend Foo
{
    public function getDate() {
        return '20170801'; // 単に値を返すだけのスタブ
    }
}

オートロードが働くようにcomposer.jsonautoload-devを追加します。

"autoload-dev": {
    "psr-4": {
        "MyVendor\\MyProject\\": "tests/fake-src"
    }
},

testコンテキストで実行するとFooの代わりにFakeFooが呼ばれるようになります。

$resource = (new AppInjector('MyVendor\MyProject', 'test-app'))->getInstance(ResourceInterface::class);

スパイ

入出力を記録してスパイするクラスに@Spyとアノテートします。

<?php
use Ray\TestDouble\Annotation\Spy;

/**
 * @Spy
 */
class Calc
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

Spyクラスから(クラス名,メソッド名)を指定してgetLogs()するとメソッドの引数や実行結果が保存されているSpyLog値オブジェクトが取得できます。そのオブジェクトを使って@Spyとアノテートしたクラスの入出力や呼び出し回数をテストすることができます。

public function testSpy()
{
    $injector = (new AppInjector('MyVendor\MyProject', 'test-app'))->getInstance(InjectorInterface::class);
    $calc = $injector->getInstance(Calc::class);
    $result = $calc->add(1, 2); // 3

    // スパイログを取得
    $spy = $injector->getInstance(Spy::class);
    $logs = $spy->getLogs(Calc::class, 'add');
    $this->assertSame(1, count($logs)); // call time
    /* @var $log SpyLog */
    $log = $logs[0]; // first call log

    // メソッドがコールされた時の引数と結果を検査
    $this->assertSame([1, 2], $log->arguments);
    $this->assertSame(3, $log->result);
}

Fakeクラスをスパイしてテストダブルへの呼び出しを検査することもできます。

/**
 * @Spy
 */
class FakeUserRole extend UserRole
{
    public function getRoleById(string $id) : string
    {
        // ...条件
        return $role
    }
}

無名クラスを使った束縛

PHPの無名クラスを使って一時的に依存を束縛することができます。

public function testAnonymousClassBinding()
    $injector = new AppInjector('FakeVendor\HelloWorld', 'hal-app');
    $module = new class extends AbstractModule {
        protected function configure()
        {
            $this->bind(FooInterface::class)->to(Foo::class);
        }
    };
app');
    $index = $injector->getOverrideInstance($module, Index::class);
    $name = $index(['id' => 1])->body['name'];
    $this->assertSame('BEAR', $name);
}

スタブを束縛

phpunitのcreateMock()メソッドなどのモッキングツールでスタブを作成してそのインスタンスと束縛することもできます。

public function testStub()
{
    $injector = new AppInjector('FakeVendor\HelloWorld', 'hal-app');
    $stub = $this->createMock(FooInterface::class);
    $stub->method('doSomething')
        ->willReturn('foo');
    $module = new class($stub) extends AbstractModule {

        private $stub;

        public function __construct(FooInterface $stub)
        {
            $this->stub = $stub;
        }

        protected function configure()
        {
            $this->bind(FooInterface::class)->toInstance($this->mock);
        }
    };
    $index = $injector->getOverrideInstance($module, Index::class);
    $name = $index(['id' => 1])->body['name'];
    $this->assertSame('BEAR', $name);
}

ベストプラクティス

  • 実装ではなく、インターフェイスをテストする
  • フェイククラスを好む。スタブはOK。モックには批判的な意見もあり複雑なものを避ける。

参考URL