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 の手順を紹介していきます。
- テストを書く(テストファースト)
- 実行して失敗させる
- テストが通る実装を書く
- テストを成功させる
- テストが通る状態のままコードをきれいにする
- 実装を完成させる
TDD の流れ
TDD のサイクルについて紹介します。
- レッド:動作しないテストを 1 つ書く。
- グリーン:そのテストを迅速に動作させる。このステップでは罪を犯してもよい。
- リファクタリング:テストを通すために発生した重複をすべて除去する。
メリット
- バグが少なくなる
- デバッグの時間が短くなる
- コードを書くことで具体化できる
- きれいなコードを作成できる
デメリット
- 実践するのに時間がかかる
- コーディング時間が伸びる
- テストするのが難しいケースがある
- テストコードの保守が必要
インストールする
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 は良いですね。
ここまで読んでいただき、ありがとうございました!