前のパートに戻る 完了して次のパートへ  

  1-3 テスト駆動でユニットテストを書いてみる

ユニットテストでは、単一あるいは少数の小さなクラスの集まりが提供する機能(関数)の動作を確認します。

原則的には、フレームワークやアプリケーション以外の要素(データベースや外部 APIなど)に依存しないようにテスト対象を組み立てますが、そうした要素を切り離すことでかえって設計が複雑になるようであれば、それらを含めた上でユニットテストを書くこともあります。


要件の確認とテストケースの洗い出し


最初の例は単純なものにします。

0-6 設計で定義したとおり、今回の題材であるヨガスタジオ予約サイトには、

空き状況を表す記号を残り枠数に応じて表示する

という要件があります。記号の出し分けは以下のルールです。

  • 残り枠数 0 = ×
  • 残り枠数 1 以上 5 未満 = △
  • 残り枠数が 5 以上 = ◎

テストケースとして、

  1. × になるパターン: 空きなし
  2. △ になるパターン: 残りわずか
  3. ◎ になるパターン: 空き十分

の3パターンをピックアップしてみます。


改めてテスト駆動開発のサイクルについて


前にテスト駆動開発の3つのフェーズについては書きましたがもう一度おさらいです。

「レッドフェーズ」は、テストが失敗するフェーズです。テストコードから先に書き、後からプロダクションコードを書くので、テストを書いた直後は必ず失敗します。失敗の状態から始め、すぐに成功に持っていくことで、安心してリファクタリングをすることができます。

このフェーズは設計のフェーズでもあります。クラス名、インターフェイス、テストパターンを決めます。実装のことはいったん忘れて、テスト対象のあるべき姿を、テストという姿を借りてコードとして表現してください。

「グリーンフェーズ」は、テストが成功するフェーズです。最短でテストが通る実装を素早く行うのがキモです。

「リファクタリングフェーズ」は、グリーンフェーズで雑に行った実装を正しく・きれいにしていくフェーズです。テスト駆動開発では、テストが通る=正しく実装できている、ではありません。


レッドフェーズ


まずはテスト対象のクラス名を決めなければなりません。

「空き状況」という言葉を用いて決めます。まず単純に英訳すると "availability" や "vacancy" あたりが候補になりますが、「段階に分かれている」ことを表現できていません。なので、 "level" という単語をつけてみます。 "availability level" でも "vacancy level" でも、どちらでもよさそうですが、今回は "vacancy level" にしておきます。

クラス名が決まったら命名規則に従ってテストクラスを作ります。

# php artisan make:test --unit Models/VacancyLevelTest

VacancyLevel クラスはまだつくっていませんが、インターフェイスを先に決めていきます。

コンストラクタに残り枠数を受け取り、 mark() というメソッドで、枠数に応じた記号を返すインターフェイスを考えてみました。その上で、上記3つのパターンを順にチェックしています。

tests/Unit/Models/VacancyLevelTest.php を以下のように編集してください。

4, 5 は境界値を使いました。

ここで一度テストを実行します。

# php artisan test tests/Unit/Models/VacancyLevelTest.php
   FAIL  Unit\Models\VacancyLevelTest
  ✕ mark

  Tests:  1 failed

   Error 

  Class 'App\Models\VacancyLevel' not found

  at tests/Unit/Models/VacancyLevelTest.php:12
     8| class VacancyLevelTest extends TestCase
     9| {
    10|     public function testMark()
    11|     {
  > 12|         $level = new VacancyLevel(0);
    13|         $this->assertSame('×', $level->mark());
    14| 
    15|         $level = new VacancyLevel(5);
    16|         $this->assertSame('△', $level->mark());


このような結果になるはずです。

グリーンフェーズ

続いて、プロダクションコードを書いていきます。最初は各テストケースがパスする最も単純な方法で記述していきます。

まず以下のコマンドを実行して、モデルを作成してください。

# php artisan make:model Models/VacancyLevel

作成したら、 app/Models/VacancyLevel.php を以下のように編集してください。

最初は、各テストケースが通る最速の実装を心がけてください。上の例は極端かもしれませんが、あまり深く考えず、まずはテストを通すことを第一に目指すのがテスト駆動開発の流儀です。

目の前の目標は完璧な解を出すことではなく、テストを通すことだ。代わりに犠牲になった正しさや美しさは、後から追求することにしよう。
Kent Beck. テスト駆動開発. 和田卓人訳. オーム社, 2017

テストを実行すると、こんな感じの出力が出るはずです。

# php artisan test tests/Unit/Models/VacancyLevelTest.php
  PASS  Unit\Models\VacancyLevelTest
  ✓ mark

  Tests:  1 passed
  Time:   0.42s

テストが通りました!

リファクタリングフェーズ

提示されたルールに則って、 mark() の中身を以下のように書き換えます(この実装はいくつかやり方があると思います。ご自分が納得のいくまで試行錯誤をしてみてください)。

app/Models/VacancyLevel.php

再度テストを実行し、パスすることを確認してください。テストが通っている間はどれだけ手を加えてもいいですが、リファクタリングのやり過ぎには注意してください。何事もコストとベネフィットのバランスが大事です。

ついでにテストコードもリファクタリングしましょう。PHPUnit ではデータプロバイダという仕組みを使って、テストパターンを配列で定義し、テストメソッドに順に渡してテストを行うことができます。以下のように @dataProvider メソッド名 の形式で、テストメソッドの DocComment 内に書いてください。

一階層目のキーはテストケースを表します(今回の例では「空きなし」「残りわずか」「空き十分」の3パターンがあります)。

各テストケースの連想配列のキー(今回の例では 'remainingCount' と 'expectedMark' )はテストメソッドの引数名と対応しています。キーを指定せず、普通の配列として定義することもできますが、キーを指定しておくと対応が分かりやすいので、指定しておいたほうがいいでしょう。

tests/Unit/Models/VacancyLevelTest.php

実行結果は以下のようになります。

# php artisan test tests/Unit/Models/VacancyLevelTest.php

   PASS  Unit\Models\VacancyLevelTest
  ✓ mark with data set "空きなし"
  ✓ mark with data set "残りわずか"
  ✓ mark with data set "空き十分"

  Tests:  3 passed
  Time:   0.30s

課題

CSS のクラスとして使える文字列を出力する slug() というメソッドを VacancyLevel クラスに追加してください。

使用例)

文字列の出し分けは以下のルールです(記号と同じ)。

  • 残り枠数 0 = empty
  • 残り枠数 1 以上 5 未満 = few
  • 残り枠数が 5 以上 = enough

テストケースとして、

  1. empty になるパターン: 空きなし
  2. few になるパターン: 残りわずか
  3. enough になるパターン: 空き十分

の3パターンを用意してください。

条件は同じなので、できれば条件分岐はひとつ( mark か slug いずれか)で済むように書けるか考えてみてください。もちろんテストファーストで!

この課題の解答例はこの章の末尾に載せますので、参考にしてください。

議論

2 質問

このコースの評価は?