Skip to content

Instantly share code, notes, and snippets.

@yano3nora
Last active August 13, 2018 07:54
Show Gist options
  • Save yano3nora/de844868ad9d399407c1f60542c05480 to your computer and use it in GitHub Desktop.
Save yano3nora/de844868ad9d399407c1f60542c05480 to your computer and use it in GitHub Desktop.
[cakephp: TestCase & Fixture] Test by PHPUnit on CakePHP3. #cakephp #php #test

OVERVIEW

CakePHP のテストクラス群は内部的に PHPUnit を利用しており phpunit 実行時にラッパーとして機能する。CakePHP を Composer でインストールした場合 phpunit.xml.distcomposer.json での設定が予めセットされている & PagesController とかのテストも実装済みなのでいきなり composer test とか叩ける。

PagesController のテストに「 /home 時の出力 HTML に CakePHPという文字が含まれているか?」とかのアサーションがあるので開発を進めるとすぐテストがコケるので注意してね。

テストデータ ( DB ) を Fixture クラスで、実テストクラスを TestCase を継承したテストクラスで実装していく。PHPUnit については別メモ php: phpunit を参照すること。

Tips

CakePHP のバージョンと 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/vendorapp/composer.lock などを削除し composer.json を書き直して再インストールするのが良い。CakePHP のバージョンによって phpunit.xml.dist ファイルの内容も変わってくるので注意。

CakePHP のバージョンごとの依存関係については cakephp/app - packagist 参照。


FIXTURE

Bake

# 既存 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.

Sample Code

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'
    ],
    // 略
  ];

}

TESTCASE

Bake

$ bin/cake bake test <type> <name>

# <type> は以下、<name> はクラス名
# Entity
# Table
# Controller
# Component
# Behavior
# Helper
# Shell
# Task
# Cell

Sample Code

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();
    }
}

Execution via composer

$ 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

Assertions

アサーションメソッド - book.cakephp.org

/* 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.

SAMPLE CODES

Table::initialize

Bake した時の setUp() で既に出来上がってるぽい。

public function setUp()
{
    parent::setUp();
    $config = TableRegistry::exists('Users') ? [] : ['className' => UsersTable::class];
    $this->Users = TableRegistry::get('Users', $config);
}

Table::validationDefault

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
}

Table::buildRules

// 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));
}

PHPUnit の createMock() でモック作成

テストダブル - phpunit.readthedocs.io

// 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');
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment