単体テスト
Codeceptionはテストの実行環境としてPHPUnitを使用しています。したがって、PHPUnitのどのテストでもCodeceptionのテストスイートに追加できますし、実行できます。 いままでにPHPUnitテストを書いていたならば、これまで書いてきたようにするだけです。 Codeceptionは簡単な共通処理に対して、すばらしいヘルパー機能を追加しています。
単体テストの基礎はここでは割愛する代わりに、単体テストにCodeceptionが追加する特徴の基礎知識をお伝えしましょう。
もう一度言います: テストを実行するためにPHPUnitをインストールする必要はありません。Codeceptionも実行できます。
テストを作成する
Codeceptionは簡単にテストを作成する、すばらしいジェネレーターを持っています。
\PHPUnit_Framework_TestCase
クラスを継承している従来のPHPUnitテストを生成するところから始める事ができます。
以下のようなコマンドで生成されます。
php codecept generate:phpunit unit Example
Codeceptionは一般的な単体テストのアドオンを備えています、それでは試してみましょう。 Codeceptionの単体テストを作成するには別のコマンドが必要です。
php codecept generate:test unit Example
どちらのテストもtests/unit
ディレクトリーに新しくExampleTest
ファイルを作成します。
generate:test
によって作成されたテストは、このようになります。
<?php
use Codeception\Util\Stub;
class ExampleTest extends \Codeception\Test\Unit
{
/**
* @var UnitTester
*/
protected $tester;
// 各テスト前に実行される
protected function _before()
{
}
// 各テスト後に実行される
protected function _after()
{
}
}
このクラスは、はじめから_before
と_after
のメソッドが定義されています。それらは各テスト前にテスト用のオブジェクトを作成し、終了後に削除するのに使用できます。
ご覧の通り、PHPUnitとは違い、setUp
とtearDown
メソッドがエイリアス: _before
, _after
されています。
実際にはsetUp
とtearDown
メソッドは、親クラスの\Codeception\TestCase\Test
クラスに実装されており、さらに単体テストの一部として実行できるように、Ceptファイルからすべてのすてきなアクションを持ったUnitTesterクラスがセットアップされています。受け入れテストや機能テストのように、unit.suite.yml
の設定ファイルの中でUnitTester
クラスが使う適切なモジュールを選べます。
# Codeception Test Suite Configuration
# suite for unit (internal) tests.
class_name: UnitTester
modules:
enabled:
- Asserts
- \Helper\Unit
従来の単体テスト
Codeceptionの単体テストは、PHPUnitで書かれているのとまったく同じように書かれています。:
<?php
class UserTest extends \Codeception\Test\Unit
{
public function testValidation()
{
$user = User::create();
$user->username = null;
$this->assertFalse($user->validate(['username']));
$user->username = 'toolooooongnaaaaaaameeee';
$this->assertFalse($user->validate(['username']));
$user->username = 'davert';
$this->assertTrue($user->validate(['username']));
}
}
モジュールを使う
シナリオ駆動の機能テストや受け入れテストの中で、あなたはアクタークラスのメソッドにアクセスできました。もし結合テストを書く場合は、データベースをテストするDb
モジュールが役に立つかもしれません。
# Codeception Test Suite Configuration
# suite for unit (internal) tests.
class_name: UnitTester
modules:
enabled:
- Asserts
- Db
- \Helper\Unit
UnitTesterのメソッドにアクセスする事で、テストの中でUnitTester
のプロパティを使用できます。
データベースをテストする
それでは、どのようにデータベースのテストができるのか、見て行きましょう:
<?php
function testSavingUser()
{
$user = new User();
$user->setName('Miles');
$user->setSurname('Davis');
$user->save();
$this->assertEquals('Miles Davis', $user->getFullName());
$this->tester->seeInDatabase('users', ['name' => 'Miles', 'surname' => 'Davis']);
}
単体テストでデータベース機能を有効にするためには、unit.suite.yml設定ファイルにて有効なモジュール一覧にDb
モジュールが含まれていることを確認してください。
受け入れテストや機能テストのように、データベースはテストが終了するごとに、クリーンにされて構築されるでしょう。
それが必要のない振る舞いであれば、現在のスイートのDb
モジュールの設定を変更してください。
フレームワークとやりとりする
もしプロジェクトがすでにデータベースとのやりとりのためにORMを利用しているのであれば、データベースに直接アクセスすべきではないはずです。
テストの中で直接ORMを使ってみませんか?LaravelのORMであるEloquentを使ってテストを書いてみましょう。そのためにはLaravel5モジュールを設定する必要があります。amOnPage
やsee
といったWebに対する振る舞いは必要ないため、ORMのみ有効化しましょう:
class_name: UnitTester
modules:
enabled:
- Asserts
- Laravel5:
part: ORM
- \Helper\Unit
機能テストと同じように、Laravel5モジュールをインクルードしました。実際のテストでの使い方を見てみましょう:
<?php
function testUserNameCanBeChanged()
{
// フレームワークからユーザーを作成、このユーザーはテスト後に削除される
$id = $this->tester->haveRecord('users', ['name' => 'miles']);
// モジュールにアクセスする
$user = User::find($id);
$user->setName('bill');
$user->save();
$this->assertEquals('bill', $user->getName());
// フレームワークの関数を使ってデータが保存されたことを検証する
$this->tester->seeRecord('users', ['name' => 'bill']);
$this->tester->dontSeeRecord('users', ['name' => 'miles']);
}
ActiveRecordパターンで実装されたORMを持つすべてのフレームワークにおいて、とても良く似たアプローチをとることができます。
それらはYii2やPhalconで、同じように動作するhaveRecord
、seeRecord
、dontSeeRecord
を持っています。機能テスト用のアクションを利用しないよう、part: ORM
を指定してインクルードしてください。
DoctrineとともにSymfonyを利用するケースでは、Symfonyそのものは有効とせず、Doctrine2のみを利用するようにしてください。:
class_name: UnitTester
modules:
enabled:
- Asserts
- Doctrine2:
depends: Symfony
- \Helper\Unit
このようにすることで、Doctrineはデータベースへの接続確立のためにSymfonyを使いながら、Doctrine2モジュールのメソッドを利用することができます。このケースではテストは次のようになります。:
<?php
function testUserNameCanBeChanged()
{
// フレームワークからユーザーを作成、このユーザーはテスト後に削除される
$id = $this->tester->haveInRepository('Acme\DemoBundle\Entity\User', ['name' => 'miles']);
// モジュールにアクセスしてEntity Managerを取得する
$em = $this->getModule('Doctrine2')->em;
// 実際のユーザーを取得する
$user = $em->find('Acme\DemoBundle\Entity\User', $id);
$user->setName('bill');
$em->persist($user);
$em->flush();
$this->assertEquals('bill', $user->getName());
// フレームワークの関数を使ってデータが保存されたことを検証する
$this->tester->seeInRepository('Acme\DemoBundle\Entity\User', ['name' => 'bill']);
$this->tester->dontSeeInRepository('Acme\DemoBundle\Entity\User', ['name' => 'miles']);
}
どちらの例においても、テスト間におけるデータの永続化について心配する必要はありません。 Doctrine2、Laravel5、どちらのモジュールにおいてもテストの終了時に作成されたデータはクリーンアップされます。 これは、トランザクションでテストをラッピングし、その後、それをロールバックすることによって行われます。
モジュールにアクセスする
Codeceptionはこのスイートにおいて、すべてのモジュールに定義されたプロパティとメソッドにアクセスする事を許可しています。このときはUnitTesterクラスを使うときとは違い、直接モジュールを使用する事で、モジュールのすべてのパブリックなプロパティへのアクセスを得られます。
この話は、先ほどのコードでDoctrine2モジュールからEntity Managerへのアクセスを行ったように、すでに検証しました。
<?php
/** @var Doctrine\ORM\EntityManager */
$em = $this->getModule('Doctrine2')->em;
もしSymfony
を使うなら、このようにSymfonyのコンテナにアクセスします:
<?php
/** @var Symfony\Component\DependencyInjection\Container */
$container = $this->getModule('Symfony')->container;
有効化されているモジュールのすべてのPublicプロパティについても同じようにアクセスすることができます。アクセス可能なプロパティはモジュールリファレンスに列挙されています。
BDD Specテスト
テストを書くときは、アプリケーションにおける一定の変化のためにテストを準備する必要があります。テストは読みやすく維持されやすくするべきです。あなたのアプリケーションの仕様が変わったら、同じようにテストもアップデートされるべきです。ドキュメントのテストにおいてチーム内部で話し合いが持たれなかったのならば、新しい機能の導入によってテストが影響を受けるということを理解していくのに壁があるでしょう。
そのため、アプリケーションを単体テストで網羅するだけでなく、テスト自体を説明的に保っておくことはとても重要な事です。私たちは、シナリオ駆動の受け入れテストと機能テストでこれを実践しています。そして、単体テストや結合テストにおいても同様にこれを実践するべきです。
この場合において、単体テスト内部の仕様を書いているSpecify(pharパッケージに含まれている)というスタンドアロンのプロジェクトを用意しています。
<?php
class UserTest extends \Codeception\Test\Unit
{
use \Codeception\Specify;
private $user;
public function testValidation()
{
$this->user = User::create();
$this->specify("username is required", function() {
$this->user->username = null;
$this->assertFalse($this->user->validate(['username']));
});
$this->specify("username is too long", function() {
$this->user->username = 'toolooooongnaaaaaaameeee';
$this->assertFalse($this->user->validate(['username']));
});
$this->specify("username is ok", function() {
$this->user->username = 'davert';
$this->assertTrue($this->user->validate(['username']));
});
}
}
specify
のコードブロックを使用する事で、テストを細かい単位で説明することができます。このことはチームの全員にとってテストがとても見やすく、理解しやすい状態にしてくれます。
specify
ブロックの内部にあるコードは分離されています。上記の例だと、$this->user
(他のどんなオブジェクトやプロパティでも)への変更は他のコードブロックに反映されないでしょう。Specifyは分離されたスコープ間でオブジェクトを保存する際の手段として、ディープコピーとシャローコピーを使用します。ディープコピーを使用した場合はメモリ消費量の増加が、シャローコピーを利用した場合はスコープを完全に分離できない、という欠点があります。テストで使用する前に、Specifyがどのように動作するのか、そしてどのように設定を行うのか
理解してください。
あなたはBDD-styleのアサーションをするために、Codeception\Verifyも追加するかもしれません。もし、あなたがassert
の呼び出しの中で、引数のどちらが期待している値で、どちらが実際の値なのかをよく混同してしまうなら、この小さなライブラリーはとてもすばらしく可読性に長けたアサーションを追加します。
<?php
verify($user->getName())->equals('john');
Cest
PHPUnit_Framework_TestCase
を継承したテストケースの代わりに、Codeception特有のCest形式を使用できるでしょう。他のどのクラスも継承する必要はありません。このクラスのすべてのパブリックなメソッドがテストです。
上記の例をシナリオ駆動の形式でこのように書きなおすことができます:
<?php
class UserCest
{
public function validateUser(UnitTester $t)
{
$user = $t->createUser();
$user->username = null;
$t->assertFalse($user->validate(['username']);
$user->username = 'toolooooongnaaaaaaameeee';
$t->assertFalse($user->validate(['username']));
$user->username = 'davert';
$t->assertTrue($user->validate(['username']));
$t->seeInDatabase('users', ['name' => 'Miles', 'surname' => 'Davis']);
}
}
$t
変数でアクセスしているであろう、UnitTesterクラスのいつものアサーションを追加するAsserts
モジュールを単体テストのために追加するかもしれません。
# Codeception Test Suite Configuration
# suite for unit (internal) tests.
class_name: UnitTester
modules:
enabled:
- Asserts
- Db
- \Helper\Unit
例外をチェックをするためにはAsserts
モジュールのexpectException
メソッドを使うことができます。PHPUnitの同様のメソッドとは異なり、このメソッドはテスト内部でスローされた例外をアサートします。次のコードは例外の発生処理をクロージャーに内包しています。
<?php
$t->expectException(new Exception, {
throw new Exception;
});
スタブ
Codeceptionは、スタブを簡単に作成するPHPUnitモックフレームワークの小さいラッパーを提供しています。\Codeception\Util\Stub
を追加して、ダミーオブジェクトの作成を始めてください。
この例では、コンストラクタを呼び出さずにオブジェクトを初期化し、getName
メソッドがjohnという値を返すように置き換えています。
<?php
$user = Stub::make('User', ['getName' => 'john']);
$name = $user->getName(); // 'john'
スタブはPHPUnitのモックフレームワークから生成されます。Mockery(Mockery moduleとセット)、AspectMock、など他のものを代わりに使用することもできます。
スタブのユーティリティークラスの全リファレンスはここを見てください。
まとめ
テストスイートの中で、PHPUnitのテストはfirst-class citizensです。単体テストを書いて実行したいときはいつでも、PHPUnitをインストールする必要はなく、そのままCodeception上で実行することができます。いくつかのすばらしい特徴は、Codeceptionモジュールを統合する事で共通の単体テストを追加できることです。単体テストや結合テストのほとんどにおいて、PHPUnitのテストだけで十分です。PHPUnitのテストは速く、維持しやすいからです。