テスト駆動開発の目的は「自信を伴うコードを書く」こと

nunulk氏(以下、nunulk):「Laravelでテスト駆動開発を行う際の守破離と序破急」というタイトルでお話しします。PHPカンファレンス沖縄は前回も参加して、自分としては沖縄大好き芸人枠で参加が義務で、今回はあまりネタが思い浮かばなかったんですが、がんばってスライド作りました。

今は沖縄で仕事もいただいていて、Alpaca.Labという会社で「AIRCLE」という運転代行のサービスのバックエンドを担当しています。沖縄は車がないとけっこう不便なので、沖縄にお越しの際はぜひレンタカーを借りて、この「AIRCLE」使ってみてください。

それから、もう1つ宣伝です。「Techpit」というプラグラミング教材の、CtoCの売買サイトでテスト駆動開発の教材を出しています。もし興味ある方がいたら、チェックしてみてください。無料で見られる部分もあるので、プレビューで見て、気に入って買ってもらえたらなと思います。

本日話すことはこの3つです。守破離と序破急に関しては、単にこの単語を使いたかったというだけで、あまり深い意味はないので、深く考えないでください。

ゴールは、この3つです。すでにテスト駆動開発やっている方は、ゴールを達成しているのですが、「まだやったことない」「テスト駆動ってなんだ」という方は聞いてもらえればなと思います。

テスト駆動開発はKent Beckという人が提唱したソフトウェア開発の技法です。2002年に原著が出ていて、2017年には和田卓人さんが訳された新訳が出て、第2次テスト駆動開発ブームが一瞬あったかなという感じです。今はこれをとりあえず読んでいけばいいのではないかなと思っています。

有名なRed、Green、Refactorというサイクルなんですが、まず「失敗するテスト書いて」「小さな変更を行って、テスト通して」「重複を除去する」という3つのサイクルを繰り返すのが基本です。

TDDをたまに誤解されている方がいるのですが、TDDはテストに関する技法ではなくて、ソフトウェア開発のための技法です。その手段であって、目的ではありません。目的は「自信を伴うコードを書く」です。

個人的には、テスト駆動開発がもたらすものとして、この3つがいいなと思っています。テストが設計のガイドになることと、コードが壊れたら、テストが教えてくれるので安心感が得られます。テストがグリーンの間はリファクタリングができるので、チームやプロダクトによって求められるレベル感は変わるかなと思うのですが、調整しながらコードを育てていけるところがメリットかなと思います。

Laravelの2種類のテスト「Unit Test」「Feature Test」

PHPUnitは自動テストのデファクトツールですが、Laravelにもこれがバンドルされていて、Unit TestとFeature Testという2つの区分で書くのが公式の推奨です。もともとそういうディレクトリがインストール時点で用意されているので、その流れに沿っていくと、Unit TestとFeature Test2つ書いていくことになります。

Unit Testは、基底クラスのuse文を見るとわかるとおり、PHPUnitのクラスを継承していて、使い方もLaravel固有のものはありません。ただ、Unit Testでも、Feature Test同様、Laravelが提供する基底クラスを継承して使うことは可能なので、場合によって使い分けていくことになると思います。使い方も、普通のPHPUnitの使い方と同じですね。

一方Feature Testは、Laravelによって拡張がされています。例えばこのRefreshDatabaseというトレイトなど、便利な機能が提供されていて、基本的にはそれを使っていく流れになると思います。

ModelFactoryでデータベースにデータを作ってから、テストしたいAPIを実行して、返ってきたレスポンスに対してアサーションする流れが基本です。このModelFactoryや、actingAsというメソッドや、レスポンスに対するassertOkみたいものはすべてLaravelの拡張で、PHPUnit自体のものではありません。

今言った3つ以外にも、例えばメール送信や、ストレージにファイルを保存など、一般的にテストがしにくいと言われているものをテストしやすくする仕組みだったり、拡張したテストケースに対して、assertDatabaseHasとか、Missingとかメソッドを追加することで、メソッドを実行後にデータベースにデータが追加されているか、追加されてないか、みたいなテストができるようになっています。

LaravelでTDDをどうやるのか

では、PHPUnitを使ってLaravelでTDDする時に、どんな感じでやっていくのがいいかをお話しします。一応、守破離、序破急の説明も書いてあるのですが、あまり深く考えないでいいかなと思います。ただ何事も、その基礎ができたうえで応用することが大事かなと思うので、いったんは基礎を学んで、自分流に発展させていくことを意識しといたほうがいいのかなと思っています。

序破急は、ちょうどTDDのサイクルがRed、Green、Refactorの3つで、序破急にたまたま合っていたので、当てはめました。

「守」でいうと、「失敗するテストを書いて」「小さな変更でテスト通して」「重複を除去する」というのが基本のパターンです。これに関して、どんな感じなのかデモをしたいと思います。

お題は運転代行の運転見積もりです。「AIRCLE」がこういう仕様というわけではなくて、単にお題探していて、よさそうと思ったので新たに架空のサービスとして作りました。「地域ごとに料金体系が変わる」「出発地点で料金体系決まる」「移動距離」。最初に出発地点と到着地点がわかっていれば、だいたいの移動距離がわかるので、それに応じて料金計算を事前に出して、見積もりこれぐらいになりますよと出すAPIですね。

料金計算はロジックが存在するので、ここはUnit Test書いておいたほうがいいよね、ということでAPIの作成の前に運賃計算するだけのクラスを作って、それをテストしていきましょうというシナリオです。

Unit Testをスケルトンで生成すると、こういうコードが生成されるのですが、そこをちょっとずつ変えていって、最初のテストを書いていきます。

クラス名どうするかはけっこう悩むところだと思います。そこもあとから良いのが思いついたら変えていけばいいので、最初は適当に作っていきます。CalculateDrivingFareというクラスを作って、メソッド名も適当に最初はapplyという名前をつけていきます。

インターフェイスは、料金体系と移動距離の2つをとっていきます。料金体系も名前を考えるのが面倒くさいのでparamsにします。初乗りが1,200円なので、1,200円の時は初乗り料金500円という結果になってほしいよね、というところで書いていきます。

最初は何も定義がないので、エラーになるはずです。ここでテストを実行すると、予想どおり「Call to undefined method apply」と出ます。この予想どおりエラーになるというのが大事です。自分の書いたコードのどういうところが悪いかを予想できたほうがいいです。ダメだった場合は、何かがおかしいと、その時点で調査するといいと思います。

それで、それに合うようなコードを書いていくのですが、この時に定数を返しちゃいます。もう1回実行すると、テストが通ります。これが最初の変更で、コンパイル、インタープリターがエラーにならないように返す方法です。

例えば、移動距離が1メートル増えたら、追加料金が発生するので600になってほしいよね、というとこでエラーになります。「500 is identical to 600」。600を期待したけど500と返ってきたぞというエラーですね。ここにロジックを追加していきます。

例えば、移動距離が初乗り距離以下だったら、初乗り料金を返します。それ以外であれば600。はい、テストが通りました。

というかたちで、ワンステップずつテストコードを書いては修正するサイクルを回していきます。

不安じゃない部分は一気にやって不安なところだけTDDでやる

Feature Testも同じなので、ちょっと省いて次に行きます。「破」の序破急ということで、これは誰かが言っているというわけではなくて、僕が自分で体験してやっている方法です。「序」は「失敗するテストを1つ書く」の前に、「現時点で明らかなところまで実装して、失敗するテストを書く」。そして「小さな変更を行いテストを通す」「思いつく最善の方法でテストを通す」。それで「重複を除去する」の代わりに「やれるところまで改善する」というステップに変えています。

けっこう細かいステップをどんどん回していくので、面倒くさいと感じるところも出てくるかなと思います。慣れてきたら途中のステップをすっ飛ばして、明らかなところはやっちゃうというかたちがいいかなって思っています。

というのは、テスト駆動開発で得られるものと書いたとおり、安心感が得られるというところが大きなものなので、不安だったらテスト書く、TDDでやるという流れに僕はしています。1つの機能開発でも、不安な部分と不安じゃない部分があるので、不安じゃない部分は一気にやってしまって、不安なところだけTDDのお約束に則ってやるというかたちにしています。

APIは、レスポンスコード、レスポンスボディのチェックをするのですが、出発地点と到着地点の緯度経度情報をもらって、見積もりの金額を返すというロジックになっています。最初にスケルトンで生成すると、こんな感じのコードが生成されるはずで、APIの形式はPOSTメソッドで、URLも決まっています。レスポンスの形式も決まっています。

こんな感じで書いてしまいます。ドライバーのログインが必要なので、ドライバーの情報も登録してしまいます。

プロダクトの中でどれがボイラープレートなのかは、やっていくうちにはっきりすると思うので、そういうボイラープレート的なやつはあらかじめ全部書いておきます。

最初はパラメーターの情報は考えずに、とりあえずダミーのデータを入れておくといいと思います。次がプロダクションコードに入っていきます。メソッドも、最初はLaravel共通のrequestクラスを入れてます。ここはお作法に則って、最小の変更で通すようにしてみます。

何かを忘れてたり、自分の予想とハズレたりした場合はテストがこけるので、それが目印になるかなと思います。実際のロジックは、緯度経度の情報と、地域ごとの決まりがあるので、それをデータベースに作っていきます。

ここはどういうテーブル構造にしたらいいかなとか、どういうインターフェイスにしようかなとか、考える必要があるかなと思うので、考えながら作っていくかたちになると思います。それで、先ほどのarrayで渡していたやつをデータベースに入れておきたいので、こういうかたちで定義しています。

今度はプロダクションコードをいじっていきます。先ほど作ったCalculateDrivingFareがこういうインターフェイスになって、最終形としてこんな感じになっていきます。これでテストを実行すると、通ります。

少しステップを省いてはいますが、さっきのものに比べて、スピードが速いのではないかなと思います。悩むべきところと、悩まなくていいところ。最初のユーザーを作ってログインさせる、みたいなところは悩む必要がなくて、この運賃情報をどうもたせるかが悩む必要があるところなので、ここは基本どおりにやるということですね。

ここからrequestクラスをきちんと作ったほうがいいよねとか、リファクタリングフェーズに入っていきます。きちんと作った場合のプログラムは、このGeolocationの生成を他のrequestクラスにさせたり、距離を計算するのを、他のクラスにさせたり、DIで埋め込みたいという要求があったら、こういうかたちに変更したり……。ここらへんは、好きなようにリファクタリングします。テストが通っている間は、正しくリファクタリングできていると信じて、やっていくかたちになろうかと思います。

これだけだと1つのパターンしかできません。複数のパターンをテストしたい場合は、Unit Testで、データプロバイダを使ってテストを増やしていきます。最終的にできるのはこんな感じです。

PHPUnitのデータプロバイダという仕組みを使うと、テストパターンを複数渡すことができます。それをこんな感じで使うと、テストメソッドは1つで、データのパターンが複数渡せる、みたいな感じになります。それぞれテストしたい項目をここに書きます。もし将来的に不具合があったとしても、テストケースを増やしていくことでそこを潰せるので、積極的に使っていくといいのかなと思っています。

試行錯誤を続けて自分がいいと思えるところまでやる

そろそろまとめに入ります。「離」というのは離れるという意味なので、すべてのステップが自分がいいと思えるところまでやるというかたちでいいんじゃないかなと思っています。それまでは試行錯誤をいろいろ続けて、熟達していければいいのかなと思います。

TDDの目的は「大いなる自信を伴うコードを得ることである」ということと、テスト駆動開発がもたらすものは、「設計のガイド」や「安心感」「動作するきれいなコード」ですよということです。

ぜひ、自分が安心できる、自分なりのやり方を見つけてもらえればなと思います。発表はここまでにしたいと思います。ありがとうございました。

司会者:ありがとうございます。「どうしてもTDDだと時間がかかってしまうように思ってしまう」というツイートがありましたが、これに対して何かありますか?

nunulk:そうですね。慣れるまで時間がかかってしまうのはしょうがないかなと思うので、最初は例えばAPIのテストだけ書くとか、200番で返ってくることだけ確認するとか。徐々にその範囲を広げていくのがいいかなと思います。

あとはどういうモジュールに対してテスト書くのがいいか、みたいな優先順位は決められるかなと思います。優先度の高いやつだけ書くとか、やりようはあるかなと思うんですよね。

だから、サービスとかプロダクトで重要度が高いとか、あとはそのパターンがいっぱいあって毎回手動テストするのが面倒くさいとか、そういうところに時間かけて書いて、あとは書かなくてもいいみたいな判断はできるかなと思います。

司会者:ありがとうございます。僕の話になっちゃうのですが、nunulkさんのTechpitのやつを見てやってみて、すごくわかりやすかったです。ステップ方式で、最初にわざとエラーが来るところからやっていくので、すごくわかりやすかったと思いました。

nunulk:ありがとうございます。