よしかわーるど

プログラミングで世界を変える

2018-10-21

CakePHPでTDDする

CakePHP で TDD する

どうも、最近、お布団から全然出れないよしかわです。

今回は CakePHP でテスト駆動開発(以下 TDD と称する)をした体験談をまとめていきます。

TDD をすると、どんなメリット、デメリットがあるかに言及していきます。

対象読者

  • TDD を知らない人
  • CakePHP を業務で触っている人
  • 研究で TDD を取り入れたい人

TDD とは

テスト駆動開発(Test-Driven Development)とは、プログラム開発手法の一種で、プログラムに必要な各機能について、最初にテストを書き、そのテストが動作する必要最低限な実装をとりあえず行った後、コードを洗練させる、という短い工程を繰り返すスタイルである。(Wiki 引用

Clean code that works. 「動作するきれいなコード」。Ron Jeffries の言葉が、TDD の目標です。

TDD はシンプルな 2 つのルールです。

  • 自動化されたテストが失敗したときのみ、新しいコードを書く。
  • 重複を除去する。

TDD の手順を紹介していきます。

  1. テストを書く(テストファースト)
  2. 実行して失敗させる
  3. テストが通る実装を書く
  4. テストを成功させる
  5. テストが通る状態のままコードをきれいにする
  6. 実装を完成させる

TDD の流れ

TDD のサイクルについて紹介します。

  • レッド:動作しないテストを 1 つ書く。
  • グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。
  • リファクタリング:テストを通すために発生した重複をすべて除去する。

TDDの黄金回転

メリット

  • バグが少なくなる
  • デバッグの時間が短くなる
  • コードを書くことで具体化できる
  • きれいなコードを作成できる

デメリット

  • 実践するのに時間がかかる
  • コーディング時間が伸びる
  • テストするのが難しいケースがある
  • テストコードの保守が必要

インストールする

CakePHP では PHPUnit を導入することができます。

Composer で簡単に導入してみましょう。

php composer.phar require --dev phpunit/phpunit:"^5.7|^6.0"

テストで使用する DB の設定をします。

config/app.php の以下の test の部分を変更します。

'test' => [
    'className' => 'Cake\Database\Connection',
    'driver' => 'Cake\Database\Driver\Mysql',
    'persistent' => false,
    'host' => 'dbhost',
    'username' => 'dbuser',
    'password' => 'password',
    'database' => 'test_database'
],

CakePHP でテストコードを自動生成

多くのフレームワークはテストコードを自動生成してくれます。

ここでは、CakePHP の bake コマンドで自動生成してみましょう。

bin/cake bake test Controller User

bake コマンドで「/tests/TestCase/Controller/UsersControllerTest.php」が生成されました。

テストしてみる

テストの実行は以下のコマンドを入力します。

vendor/bin/phpunit

まずは何も書かずに実行しましょう。

vendor/bin/phpunit

PHPUnit 6.5.13 by Sebastian Bergmann and contributors.

.IIIII....IIIIIII.                                      18 / 18 (100%)

Time: 506 ms, Memory: 15.25MB

OK, but incomplete, skipped, or risky tests!
Tests: 18, Assertions: 24, Incomplete: 12.

tests/TestCase 下のテストコードが実行されます。

失敗から成功に

最初にテストコードを書きましょう。

今回満たす要件は以下の 3 つのことです。

  • ユーザーについて確認するときに、セッションが確立されていないとリダイレクトする
  • ユーザーの追加ができる
  • セッション確立後はユーザーの編集が可能である

tests/TestCase/Controller/UsersControllerTest.php

public function testIndex()
{
    $this->get('/users');
    // Redirect from users '?redirect=/users'
    $this->assertRedirect('/users/login?redirect=%2Fusers');
}

public function testAdd()
{
    $this->post(
        '/users/add',
        [
        'name' => 'test_user',
        'email' => 'test_email@hoge.com',
        'password' => 'hogehoge'
        ]
    );
    // 2xx/3xx Check response code
    $this->assertResponseSuccess();
}

public function testEdit()
{
    $this->session(
        [
        'Auth' => [
            'User' => [
                'id' => 1,
                'name' => 'test_user',
                'email' => 'test_email@hoge.com',
                'password' => 'hogehoge',
                'created_at' => new FrozenTime('2018-09-25 10:26:13'),
                'updated_at' => new FrozenTime('2018-09-25 10:26:13')
            ]
        ]
        ]
    );
    // OK accessing with GET method
    $this->get('/users/edit/1');
    $this->assertResponseOK();
}

テストを通すために、ログイン機構が必要であるため、以下のようなコードになります。

src/Controller/UsersController.php

public function beforeFilter(Event $event)
{
    parent::beforeFilter($event);
    $this->Auth->allow(['add', 'logout']);
}

public function login()
{
    if ($this->request->is('post')) {
        $user = $this->Auth->identify();
        if ($user) {
            $this->Auth->setUser($user);
            return $this->redirect($this->Auth->redirectUrl());
        }
        $this->Flash->error(__('Invalid email or password, try again'));
    }
}

public function add()
{
    $user = $this->Users->newEntity();
    if ($this->request->is('post')) {
        $user = $this->Users->patchEntity($user, $this->request->getData());
        if ($this->Users->save($user)) {
            $this->Flash->success(__('The user has been saved.'));

            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('The user could not be saved. Please, try again.'));
    }
    $this->set(compact('user'));
}

public function edit($id = null)
{
    $user = $this->Users->get($id, [
        'contain' => []
    ]);
    if ($this->request->is(['patch', 'post', 'put'])) {
        $user = $this->Users->patchEntity($user, $this->request->getData());
        if ($this->Users->save($user)) {
            $this->Flash->success(__('The user has been saved.'));

            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('The user could not be saved. Please, try again.'));
    }
    $this->set(compact('user'));
}

リファクタリングをすることがなかったので、本当のTDDにはなっていないですね…

レッドからグリーンにするために、コーディングしていきましょう。

その後から、きれいなコードにしていきます。

まとめ

TDD をしたくなりましたか?

最初からきれいで動作するコードを書くのは、とてもむずかしいですよね。

僕は CircleCI を用いた TDD をしています。やっぱり研究でも役に立つ TDD は良いですね。

ここまで読んでいただき、ありがとうございました!

参考文献

テスト駆動開発

テスト駆動開発(TDD)

バグは早めに潰す!テスト駆動開発(TDD)の概要とメリット・デメリット

CakePHP test - 3.6