CakePHP のテストクラス群は内部的に PHPUnit を利用しており phpunit
実行時にラッパーとして機能する。CakePHP を Composer でインストールした場合 phpunit.xml.dist
や composer.json
での設定が予めセットされている & PagesController
とかのテストも実装済みなのでいきなり composer test
とか叩ける。
PagesController
のテストに「/home
時の出力 HTML に CakePHPという文字が含まれているか?」とかのアサーションがあるので開発を進めるとすぐテストがコケるので注意してね。
テストデータ ( DB ) を Fixture クラスで、実テストクラスを TestCase を継承したテストクラスで実装していく。PHPUnit については別メモ php: phpunit を参照すること。
PHPUnit は Composer で composer require --dev
でインストールするが、CakePHP 3.5.* では phpunit/phpunit:"^5.7|^6.0"
で CakePHP3.4 なら phpunit/phpunit:"<6.0"
など、バージョン互換に気を遣う必要がある。3.5 系の CakePHP では 7 系の PHPUnit は動作しないので注意。
依存パッケージのバージョン互換で問題が起きた場合は、対応するバージョンを調査し、一旦 app/vendor
や app/composer.lock
などを削除し composer.json
を書き直して再インストールするのが良い。CakePHP のバージョンによって phpunit.xml.dist
ファイルの内容も変わってくるので注意。
CakePHP のバージョンごとの依存関係については cakephp/app - packagist 参照。
# 既存 default DB より Fixture 生成 ( 100 件分 )
bin/cake bake fixture -r -n 100 -f -s Users
# オプション
--conditions The SQL snippet to use when importing records.
(default: 1=1)
--connection, -c The datasource connection to get data from.
(default: default)
--count, -n When using generated data, the number of records to
include in the fixture(s). (default:1)
--force, -f Force overwriting existing files without prompting.
--help, -h Display this help.
--plugin, -p Plugin to bake into.
--quiet, -q Enable quiet output.
--records, -r Generate a fixture with records from the non-test
database. Used with --count and --conditions to limit
which records are added to the fixture.
( -n でレコード数は指定しないとあかん )
--schema, -s Create a fixture that imports schema, instead of
dumping a schema snapshot into the fixture.
( これつけると public $import = ['table' => 'hoge']; で焼いてくれる )
--table The table name if it does not follow conventions.
--theme, -t The theme to use when baking code. (choices:Bake|Migrations)
--verbose, -v Enable verbose output.
namespace App\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
class ArticlesFixture extends TestFixture
{
// 現在の default 接続 DB の当該テーブルから fixture レコードをインポート
public $import = ['model' => 'Users']; // ['table' => 'users']; とかでも OK
// テスト用レコードの登録
public $records = [
[
'title' => 'First Article',
'body' => 'First Article Body',
'published' => '1',
'created' => '2007-03-18 10:39:23',
'modified' => '2007-03-18 10:41:31'
],
// 略
];
}
$ bin/cake bake test <type> <name>
# <type> は以下、<name> はクラス名
# Entity
# Table
# Controller
# Component
# Behavior
# Helper
# Shell
# Task
# Cell
namespace App\Test\TestCase\Model\Table;
use App\Model\Table\ArticlesTable;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\TestCase;
use Cake\Log\Log; // デバッグログ出力するなら追記
class ArticlesTableTest extends TestCase
{
// Fixture のロード
public $fixtures = ['app.articles']; // 他にもロードするなら ['app.articles', 'app.users']; とか
// テストメソッドの前処理
public function setUp()
{
parent::setUp();
$config = TableRegistry::exists('Articles') ? [] : ['className' => ArticlesTable::class];
$this->Articles = TableRegistry::get('Articles', $config);
}
// テストメソッド
public function testFindPublished()
{
// Table で既に実装済みのカスタム Finder メソッド `findPublished()` をテストする例
// `$this` はこのときテストクラスなので `$this->find()` でない点に注意
$query = $this->Articles->find('published');
$this->assertInstanceOf('Cake\ORM\Query', $query);
$result = $query->hydrate(false)->toArray();
$expected = [
['id' => 1, 'title' => 'First Article'],
['id' => 2, 'title' => 'Second Article'],
['id' => 3, 'title' => 'Third Article']
];
$this->assertEquals($expected, $result); // アサーション
Log::debug($result); // app/tmp/logs/cli-debug.log へ吐き出すとかね
}
// テストメソッド後処理 ( テスト DB 初期化 )
public function tearDown()
{
unset($this->Articles);
parent::tearDown();
}
}
$ cd app
$ composer test
# --filter で実行テストをフィルタリング
$ composer test -- --filter=testFindPublished tests/TestCase/Model/Table/ArticlesTableTest
# --coverage-html でコードカバレッジレポートを HTML 形式で出力
$ composer test -- --coverage-html webroot/coverage
# 色付けたりデバッグ情報出したり詳細情報出したり
$ composer test -- --colors=always --debug --verbose
/* CakePHP Assertions */
// Check for a 2xx response code
$this->assertResponseOk();
// Check for a 2xx/3xx response code
$this->assertResponseSuccess();
// Check for a 4xx response code
$this->assertResponseError();
// Check for a 5xx response code
$this->assertResponseFailure();
// Check for a specific response code, e.g. 200
$this->assertResponseCode(200);
// Check the Location header
$this->assertRedirect(['controller' => 'Articles', 'action' => 'index']);
// Check that no Location header has been set
$this->assertNoRedirect();
// Check a part of the Location header
$this->assertRedirectContains('/articles/edit/');
// Assert not empty response content
$this->assertResponseNotEmpty();
// Assert empty response content
$this->assertResponseEmpty();
// Assert response content
$this->assertResponseEquals('Yeah!');
// Assert partial response content
$this->assertResponseContains('You won!');
$this->assertResponseNotContains('You lost!');
// Assert layout
$this->assertLayout('default');
// Assert which template was rendered (if any)
$this->assertTemplate('index');
// Assert data in the session
$this->assertSession(1, 'Auth.User.id');
// Assert response header.
$this->assertHeader('Content-Type', 'application/json');
// Assert view variables
$user = $this->viewVariable('user');
$this->assertEquals('jose', $user->username);
// Assert cookies in the response
$this->assertCookie('1', 'thingid');
// Check the content type
$this->assertContentType('application/json');
上記に加えて PHPUnit や CakePHP の TestSuite クラスのアサーションが利用可能。
$this->assertEventFired() public
// Asserts that a global event was fired.
// You must track events in your event manager for this assertion to work
$this->assertEventFiredWith() public
// Asserts an event was fired with data
$this->assertHtml() public
// Asserts HTML tags.
$this->assertNotWithinRange() protected static
// Compatibility function to test if a value is not between an acceptable range.
$this->assertPathEquals() protected static
// Compatibility function to test paths.
$this->assertTags() public
// Asserts HTML tags.
$this->assertTextContains(string $needle , string $haystack , string $message '' , boolean $ignoreCase false);
// Assert that a string contains another string, ignoring differences in newlines.
// Helpful for doing cross platform tests of blocks of text.
$this->assertTextEndsNotWith() public
// Asserts that a string ends not with a given prefix, ignoring differences in newlines.
// Helpful for doing cross platform tests of blocks of text.
$this->assertTextEndsWith() public
// Asserts that a string ends with a given prefix, ignoring differences in newlines.
// Helpful for doing cross platform tests of blocks of text.
$this->assertTextEquals() public
// Assert text equality, ignoring differences in newlines.
// Helpful for doing cross platform tests of blocks of text.
$this->assertTextNotContains() public
// Assert that a text doesn't contain another text, ignoring differences in newlines.
// Helpful for doing cross platform tests of blocks of text.
$this->assertTextNotEquals() public
// Assert text equality, ignoring differences in newlines.
// Helpful for doing cross platform tests of blocks of text.
$this->assertTextStartsNotWith() public
// Asserts that a string starts not with a given prefix, ignoring differences in newlines.
// Helpful for doing cross platform tests of blocks of text.
$this->assertTextStartsWith() public
// Asserts that a string starts with a given prefix, ignoring differences in newlines.
// Helpful for doing cross platform tests of blocks of text.
$this->assertWithinRange() protected static
// Compatibility function to test if a value is between an acceptable range.
Bake した時の setUp()
で既に出来上がってるぽい。
public function setUp()
{
parent::setUp();
$config = TableRegistry::exists('Users') ? [] : ['className' => UsersTable::class];
$this->Users = TableRegistry::get('Users', $config);
}
public function testValidationDefault()
{
$data = [
'username' => 'adminadmin@mixtureweb.nl',
'password' => 'testtest123',
'sign_in_count' => 0,
'current_sign_in_ip' => '127.0.0.1',
'active' => true
];
$user = $this->Users->newEntity($data);
$this->assertEmpty($user->errors()); // empty = no validation errors
}
// public function buildRules(RulesChecker $rules)
// {
// $rules->add($rules->existsIn(['users_id'], 'Users'));
// return $rules;
// }
public function buildRules()
{
$data = [
'user_id' => 999, // Fixture に存在しない user_id
'subject' => 'lorem ipsum.',
'body' => 'lorem ipsum.',
];
$Article = $this->Articles->newEntity($data);
$this->assertFalse($this->Articles->save($Article));
}
// tests/TestCase/SomeMock.php
namespace App\Test\TestCase;
class SomeMock
{
public function hello()
{
// 後で ->method(['hello']); とかで実装する
}
}
// tests/TestCase/Model/Table/UsersTableTest.php
namespace App\Test\TestCase\Model\Table;
use App\Test\TestCase\SomeMock;
class UsersTableTest extends TestCase
{
public function testHelloMock()
{
$SomeMock = $this->getMockBuilder(SomeMock::class)
->setMethods(['foo', 'bar', 'baz', 'hello'])
->getMock();
$this->assertEquals($SomeMock->foo(), null);
$SomeMock->method('hello')->willReturn('hello');
$this->assertEquals($SomeMock->hello(), 'hello');
}
}