テストを書き始めた僕が伝えたいテストの恩恵

2ヶ月前から、僕はテストを書き始めました。とりあえず早く作れという開発にうんざりしていた毎日から抜け出すために。とりあえず実践テスト駆動開発を読んで、そして実際に実践してみました。

実践する前は、テストの一番の利点はバグを早期に発見できることかなと思っていました。しかし実際に実践してみると、もっと重要な恩恵をテストはもたらしてくれるのだと気づいたのです。

2ヶ月前の僕みたいにテストを書き始めようとしている人や、テストを書け書けってうるさく言ってる人(今の僕みたいに)がいるのだけど何でだろうと思っている人に、この記事が役に立てれば幸いです。

※この記事でのテストはユニットテストを指します。

テストを書くメリット

テストを書くメリットの中で、特に重要だと思うものだけを挙げよと言われたら、僕だったら以下の3つを挙げます。僕が重要だと思う順番に並べました。

  1. 適切な粒度、再利用可能でスケーラブル、YAGNIな設計
  2. 高速なフィードバックサイクル
  3. 書いたコードへの自信、それを変更する自信、そして複雑な技術に対峙できる自信

それぞれを解説します。

1. 適切な粒度、再利用可能でスケーラブル、YAGNIな設計

テスト駆動開発ではプロダクションコードを 書く前にテストコードを書きます。そしてこれには意味があるのです。

プロダクションコードを書く前に書くテストは、これから実装する機能が外部から使いやすいかどうかを実装者に考えさせます。

簡単な例を示した方が理解しやすいですね。「あるアイテムの最終更新日時を取得して表示する機能を作ってほしい」という要求に応えるとしましょう。この機能を要求単位で作るとこんな感じになったとします。

function showLastUpdatedDate(id) {
  var $dateBox = $("#dateBox");
  $.get("/updatedDate", {id: id}, function success(data) {
    var today = new Date.now();
    var lastUpdatedDay = Date.parse(data.lastUpdated);
    var diff = (today - lastUpdatedDay) / 24*60*60;
    $dateBox.text(diff + "日前");
  });
}

このshowLastUpdatedDate関数をload時に呼べば確かに最終更新日時を表示できます。しかしこの設計には問題があるのです。

何が問題なのかを回答する前に、このコードをテストしてみましょう。「テストは設計の問題を浮き彫りにする」ので、このコードのテストケースを書くことで設計の問題が分かりそうです。

describe("showLastUpdatedDate", function() {
  beforeEach(function(done) {
    showLastUpdatedDate("someId");
    setTimeout(function() {done();}, 5000);
  });
  it("showLastUpdatedDate(id)は/updatedDateから取得したデータのlastUpdatedキーから、現在から何日前に更新されたのかをdateBoxに表示する", function() {
    expect($("#dateBox").text()).toBe("3日前");
  });
});

このテストはいくつかの無理をしています。

  • showLastUpdatedDateがいつ完了するかが分からないので、setTimeoutでとりあえず5秒後には表示されているだろうと過程している。しかし実装では5秒後に処理が完了することは保証してはいない。
  • このテストはshowLastUpdatedDateが最終更新日時を取得する方法を知っていることを示している。この例では/updatedDateというURLからGETで取得するのだけど、このサーバーが利用可能でないと動作しない。
  • サーバーのレスポンスが適切な形式である必要がある。この場合はlastUpdatedというキーがあるオブジェクトでないといけない。
  • かつ"someId"というIDをもつアイテムが存在し、その更新日時が”今日”から3日前でなければならない。”今日”が何日なのかによってテストの成否が左右されてしまう。
  • "dateBox"というIDをもつHTML要素が存在する必要がある。

さあもうshowLastUpdatedDate関数の設計の問題が浮き彫りになったと思います。何が問題だったのかというと、showLastUpdatedDate関数の役割が多すぎるのです。

showLastUpdatedDate関数は1. 最終更新日時の取得、2. 現在からの経過日の計算、3. 計算結果の表示、という3つの役割をもっています。でもshowLastUpdatedDate関数のメインの役割は2.の「現在からの経過日の計算」でしょう。最終更新日時をどう取得するかはどうでもいいのです。例えば最終更新日時をlocalStorageから取得しても目的は満たせるかもしれません。そして計算結果の表示場所も同様に固定しない方が汎用的に使えます。

テストが複雑になってしまいました。テストを後に書いたからですね。実装を書く前にテストから書いたらもっと簡単に書けそうなのに。

ということでテストから書いてみましょう。テストが書きやすいように「現在からの経過日の計算」の機能に集中します。

describe("diffDate", function() {
  it("diffDateは2つの日付の差を日単位で計算する", function() {
    var today = new Date("Sun, 25 Jan 2015 04:01:02 GMT");
    var lastUpdatedDay = new Date("Thu, 22 Jan 2015 04:01:02 GMT");
    expect(diffDate(lastUpdatedDay, today)).toBe(3);
  });

});

今度はdiffDateという2つの日付の差を計算することに特化した関数に対するテストケースを記述しました。これは単に2つのDateオブジェクトを受け取り、その差を日単位で計算して返しているだけです。最終更新日時の取得や、計算結果の表示の機能は排除してあります。これによってテストは単純になりました。さらに”今日”という日付も外から渡すことによって、いつ実行しても再現がとれるようになっています。

今回設計したdiffDate関数は、showLastUpdatedDate関数よりもどの点において優れているでしょうか。

  • 最終更新日時をどうやって取得したかに依存しない。ajaxから取得しようがストレージから取得しようがメモリー上のオブジェクトから取得しようが使うことができる。
  • 最終更新日時の形式に依存しない。showLastUpdatedDate関数はlastUpdatedというキーに最終更新日時が格納されていることを仮定していた。
  • 最終更新日時の計算用途に限定されない。計算しているものは2つの日付の差なので、例えば「残り何日」という計算にも使えるだろう。
  • どこに表示するかに関して無関心。どのDOM要素に表示しようがコンソールに表示しようが自由だ。もちろん表示に使わなくてもよい。

これらを一言でいうと、showLastUpdatedDateに比べてdiffDateは再利用可能だということです。

再利用可能性の実例を見せましょう。最終更新日時が1日を切ったときに、○時間前、○分前というように表示したほうがよいという要求が来ることは想像に難くないですね。

例によってテストから書きます。

describe("diffDate", function() {
  var today = new Date("Sun, 25 Jan 2015 04:01:02 GMT");
  
  it("diffDateは2つの日付の差を日単位で計算する", function() {
    var lastUpdatedDay = new Date("Thu, 22 Jan 2015 04:01:02 GMT");
    expect(diffDate(lastUpdatedDay, today)).toBe(3);
  });
  
  it("diffHourは2つの日付の差を時間単位で計算する", function() {
    var lastUpdatedDay = new Date("Thu, 22 Jan 2015 01:01:02 GMT");
    expect(diffHour(lastUpdatedDay, today)).toBe(3);
  });
  
  it("diffMinutesは2つの日付の差を分単位で計算する", function() {
    var lastUpdatedDay = new Date("Thu, 22 Jan 2015 04:00:02 GMT");
    expect(diffMinutes(lastUpdatedDay, today)).toBe(1);
  });

});

実装は例えば以下のようになるでしょう。

function diffDate(a, b) {
  return diffHour(a, b) / 24;
}

function diffHour(a, b) {
  return diffMinutes(a, b) / 60;
}

function diffMinutes(a, b) {
  return (b - a) / 60;
}

既存のコードをうまく再利用することができました。

再利用可能であると同時にスケーラブルとも言えます。これらの基本的な機能を組み合わせてもっと複雑なこともできるでしょう。よい設計は将来のユーザーの要求に対応するためにも非常に重要です。

さらに便利にしてみましょう。年単位、月単位、秒単位で差を計算できるようにしてみましょう。もちろんテストから書き始めましょう。

.....

めんどくさいですね。そう面倒なのです。はたして年単位、月単位、秒単位などという表示は使うのでしょうか?一度も使われない機能を実装するのは時間の無駄です。そしてバグの温床になるという危険もあります。

テストから書けば過剰な機能追加を思いとどまることができます。つまりYAGNI (You ain't gonna need it!: あなたはそれを必要としない)設計にできるのです。

2. 高速なフィードバックサイクル

テスト駆動開発では開発サイクルを高速に回すことができます。

テスト駆動開発へのよくある批判に「テストを書く工数が発生し開発を遅らせる」というものがあります。しかしこれは間違いです。

たしかにテストコードは書く必要があります。しかしそれはとても単純なものです。1つのテストケースを書くのに要する時間はたった数分です。もしテストがあまりに複雑になる場合は、前述したように設計に問題がある可能性があります。

そのテストコードを書くことに使った時間は、その後の高速なフィードバックサイクルで裕にとりかえすことができます。テストを実行すると、特定の機能の振る舞いが要件を満たしているかがチェックされ、瞬時にその結果の一覧を教えてくれます。そして失敗しているテストをパスするようにプロダクションコードを修正していきます。このサイクルはとても短いのです。

このテスト駆動開発サイクルを、デバッグプリントやブレイクポイントを使って実際にアプリケーションを実行して動作を確かめる従来の開発サイクルと比較してみてください。ブラウザでアクセスしフォーム入力などいくつもの操作して動作を確かめる、リクエストを投げてデバッグ情報を書き込んだレスポンスを見る、ブレイクポイントを張って動作を止めて関数の返り値を確かめる。これらの操作はアプリケーションを丸ごと実行し、人間の操作を伴う時間のかかる作業です。さらに確かめたい機能以外の複雑な部分も目に入ってきます。

最近のフレームワークは本当に複雑です。僕はAngularJSやReactなどを使いましたが、それぞれ固有の実行ライフサイクルをもっており、自分のコードがいつどのように実行されるのかを知ることは困難です。

また今はAkkaを使った並列分散処理アプリケーションを作っています。このアプリケーションでは処理が複数のスレッド、複数JVM複数のサーバー上で実行されるので、アプリケーションを実行してデバッグするスタイルでは処理の結果や状態を把握しきれません。

このような複雑なアプリケーションの開発下でも、雑音を断ち切り特定の機能の振る舞いの結果を瞬時に取得できる。それがユニットテストの利点なのです。

3. 書いたコードへの自信、それを変更する自信、そして複雑な技術に対峙できる自信

開発者が自分のコードに自信をもつことは非常に重要です。

テストをパスしていれば、あなたのコードは少なくとも用意したテストケースに対して確実に動作します。そして将来の変更に対して準備ができた設計になっているはずです。このコードへの自信はプロダクトへの自信につながります。

さらには先端技術に対する前向きな姿勢につながります。

コードレビューに対しておっくうなチームのメンバーがいました。彼女はAngularJSを使ったアプリケーションのレビューを控えていましたが、AngularJSの理解に苦しんでいました。そこで僕がテストを書き、彼女が実装するというやり方をやってみました。AngularJSの複雑さの中で、目的の情報だけを瞬時に取り出せるユニットテストに彼女は感動を覚えたようです。彼女はチームの中でテストの信者第1号になってくれました。

またユニットテストの力を信じていなければ、僕はAkkaという並列分散フレームワークの導入に舵を切らなかったでしょう。プロジェクトの開始時にその技術の全容を知るには複雑すぎる相手です。ユニットテストは複雑な全体からより単純な機能を切り出し、その状態を瞬時に伝えてくれます。ユニットテストの力を信じることで、僕たちは現代の複雑な技術と自信を持って対峙することができるのです。

まとめ

テストを実践してみてはじめに実感した恩恵は次の3つです。

  1. 適切な粒度、再利用可能でスケーラブル、YAGNIな設計
  2. 高速なフィードバックサイクル
  3. 書いたコードへの自信、それを変更する自信、そして複雑な技術に対峙できる自信

今の僕はこの3つを伝えたいのです。今後テストへの理解が深まるとともに、僕の考えをまた発信していこうと思います。

追伸:テストと設計の関係についてよく質問されるのですが、「テストが設計を導く」ことを説明することがかなり難しいです。この記事では簡素な例しか思いつきませんでした。よいサイトとかあれば教えて頂きたいです!