CodeIQ MAGAZINECodeIQ MAGAZINE

これであなたもテスト駆動開発マスター!?和田卓人さんがテスト駆動開発問題を解答コード使いながら解説します~現在時刻が関わるテストから、テスト容易性設計を学ぶ #tdd

2013.11.26 Category:CodeIQ問題解説・リーダーボード Tag: ,

  • 211
  • このエントリーをはてなブックマークに追加

和田卓人さんによるテスト駆動開発問題解説の寄稿です!

バグのないよいコードを書くには、よいテスト設計が重要です。今回は現在時刻に関する問題と、その問題で提出された実際の解答コードを紹介しながら、どのようにテスト設計し開発していくのかを解説していきます。

ゲスト解答による解答コードも公開中!
by CodeIQ運営事務局

はじめに

こんにちは、和田(@t_wada)です。今日は先日出題させていただいたTDDに関する問題の総評を行いつつ、テスト容易性設計について考えてみたいと思います。

問題文

私が出した問題は、以下のようなものでした。

問1. 下記の仕様をテスティングフレームワークを使ってテストコードを書きながら実装してください。

【仕様1】
「現在時刻」に応じて、挨拶の内容を下記のようにそれぞれ返す機能を作成したい。
(タイムゾーンはAsia/Tokyoとする)

  • 朝(05:00:00以上 12:00:00未満)の場合、「おはようございます」と返す
  • 昼(12:00:00以上 18:00:00未満)の場合、「こんにちは」と返す
  • 夜(18:00:00以上 05:00:00未満)の場合、「こんばんは」と返す

例: 13時にgreeter.greet()を呼ぶと”こんにちは”と返す

問2. 下記の仕様をテスティングフレームワークを使ってテストコードを書きながら実装してください。

※問2はオプション問題です。解答できそうでしたら挑戦してみてください。

【仕様2】
「現在時刻」と「ロケール」に応じて、挨拶の内容を下記のようにそれぞれ返す機能を作成したい。
(ただし、タイムゾーンはAsia/Tokyoのままとする)

ロケールが ‘ja’ の場合

  • 朝(05:00:00以上 12:00:00未満)の場合、「おはようございます」と返す
  • 昼(12:00:00以上 18:00:00未満)の場合、「こんにちは」と返す
  • 夜(18:00:00以上 05:00:00未満)の場合、「こんばんは」と返す

ロケールが ‘en’ の場合

  • 朝(05:00:00以上 12:00:00未満)の場合、「Good Morning」と返す
  • 昼(12:00:00以上 18:00:00未満)の場合、「Good Afternoon」と返す
  • 夜(18:00:00以上 05:00:00未満)の場合、「Good Evening」と返す

例: ロケール ‘en’ で18時(JST)に greeter.greet() を呼ぶと “Good Evening” と返す

注: この問題では、ロケールへのアクセス方法も含めた設計とテストに挑戦してみてください。
言い換えると、i18nライブラリ等の使い方を問うているのではありません。

問題URL:

https://codeiq.jp/ace/wada_takuto/q469

簡単に思えるけど?

問題文を見てみると、そんなに難しい機能には思えません。しかし、一見単純な関数/メソッドに思えても、いざテストを書こうとなると面倒な対象に化けるものがあります。今回のお題である、現在時刻が関わるロジックも、テストを書こうとすると面倒になるものの代表格です。今回のお題は、単純そうに見えて実は小さな設計判断が必要になる問題にしよう、という意図で作成しました。

現在時刻が関わるテストはなぜ難しいのか

なぜ現在時刻が関わるテストが難しいのか、あえて悪い例を書いて説明します。

例えばAさんが14時に問1のテストを書いたとしましょう。14時にはgreetメソッドは「こんにちは」と返しますから、以下のようなテストを書いてしまいがちです(コード例はJavaScriptですが、一般的なテスティングフレームワークの例と考えてください)。

    test('greet メソッド', function () {
        var greeter = new Greeter();
        assert.equal(greeter.greet(), 'こんにちは')
    });

このテストコードを書いてすぐに実行すると、もちろん通ります。機能が完成したと考えたAさんはバージョン管理システムにコミットします。Jenkinsサーバがコミットを検知してテストを走らせますが、こちらも通ります。

しかし、帰宅間際になってチームがざわつきます。Jenkinsのテストが落ちているのです。落ちたのはAさんが14時に書いたテストでした。一番直近のコミットを行ったのはBさんだったのでJenkinsはテストが落ちる直前のコミットを行ったBさんを「容疑者」として伝えますが、Bさんは全く無関係のコミットを行っただけです。真犯人は、Aさんですね。

ではなぜテストが落ちたのでしょうか。もうおわかりだと思います。18時を過ぎたからですね。

[ポイント] 良いユニットテストはRepeatable(繰り返し可能、再現可能)

現在時刻が関わるテストが難しいのは、当たり前かもしれませんが現在時刻がテスト対象の動作に影響するため、テストの結果が時刻に左右されるからです。

良いユニットテストは、Repeatable (繰り返し可能、再現可能)でなければなりません。

Repeatableであるとは、ユニットテストを実行するだけで、いつでも、何回でも同じように動くということです。環境や時間によってテスト結果が変化してしまう場合、そのテストはRepeatableではありません。
テストコードが変わっていないのに、状況によって通ったり通らなかったりするテストは、書籍『xUnit Test Patterns』ではErratic Test(不安定なテスト)と表現されています。

現在時刻に依存するテストは「ユニットテストはRepeatableであるべし」という原則に反しているのです。

つまり、現在時刻に依存するユニットテストのテスト容易性設計とは、いつでも、何回繰り返しても、何時に実行しても同じように動作するテストを書けるようにするための設計、ということになります。

考えるべきこと

では、今回のお題を解くにあたって、考えるべきことはどのようなものでしょうか。
具体的には、以下のような事柄を考えなければならないでしょう。

  • どのような外部インターフェイスにするか
  • テストデータの選択
  • どのようにテスト可能にするか(どのような内部構造にするか)

これらの点について、解答者の皆様のコードを見ながら考えていきます。

どのようなインターフェイスにするか

どのようなインターフェイスにするかというのは、言い換えれば外部から見た振る舞いをどう設計すべきか、ということです。ただ、今回のお題には少しバイアスがかかっています。インターフェイスに関しては、実は問題文の中で「例: 13時にgreeter.greet()を呼ぶと “こんにちは” と返す」と誘導していますので、多くの方がgreetという無引数のメソッドを持つGreeterというクラスを設計しています。

しかし、挑戦者された方々のうち何人かはGreeter#greetではなくGreeter#greet(time)というインターフェイスを、デフォルト引数との組み合わせで使えるように設計しています。この二つの設計選択も、テストに対して影響を及ぼします。

テストデータの選択

漫然とテストするのではなく、テストすべきデータも考えなければなりません。考えるべきは、同じ結果になる値の範囲と、その境界線です。今回のお題の仕様では、テストすべき値は以下のものが候補となるでしょう。

  • 00:00:00
  • 04:59:59
  • 05:00:00
  • 11:59:59
  • 12:00:00
  • 17:59:59
  • 18:00:00
  • 23:59:59

(でも、本当にそうでしょうか。ミリ秒はどうでしょうか。夏時間はどうでしょうか。応用問題として、考えてみてださい)

どのようにテスト可能にするか(どのような内部構造にするか)

ここまででインターフェイスは大まかに決まりましたが、この機能はまだテスト可能になっていません。

より正確に言うならば、テストは可能であるのですが、再現性のあるテストを書くための設計ができていません。再現性のあるテストを書くためには、時刻をテストから制御できるようにすることが重要です。

ここからが設計のしどころです。どのように時刻をテストから制御するか、解答者の皆様の方針は、以下のような方針に分かれました。

  • シンプルに対象メソッドの引数に渡す
  • 組み込みクラス/メソッド/関数に介入する
  • 対象クラスだけが使う組み込みクラスに介入する
  • 対象クラスのテスト用サブクラスをテスト内で作成 (Test-Specific Subclass)
  • テストを対象クラスのサブクラスにしてオーバーライド(Self Shunt)
  • 対象クラスの特定メソッド定義をテストで書き換える
  • 現在時刻へのアクセスを行うインターフェイスを抽出

ではこれから、今回はお題1に対するコードを題材に、ひとつひとつのアプローチを詳しく見ていきながら、テストコードの書き方、改善点についても都度レビューを行なっていきたいと思います。

シンプルに対象メソッドの引数に渡す

メソッドにデフォルト引数が使えたり、メソッドの呼び出し側で引数の省略等が行えるような言語では、引数が渡されなかった場合のデフォルト値として現在時刻を使うことで、特にテスト容易性のための特別な設計を行わなくともテストを簡潔に書くことができます。

対象メソッドの引数に渡す設計は何人かの方が選択しましたが、その中から ciel さんのコードを見てみましょう。

    class Greeter
        def greet(time=nil)
            time ||= Time.now
            hour = time.hour
            return 'おはようございます' if 5<=hour && hour<12
            return 'こんにちは' if 12<=hour && hour<18
            return 'こんばんは'
        end
    end

分量が少なく、簡潔で読み下せるコードになっているところが好印象ですね。ciel さんのテストコード (RSpec) は以下のようになっています。

    describe Greeter do
        before do
            @greeter=Greeter.new
        end
        specify 'morning' do
            (5...12).each{|i|
                @greeter.greet(Time.mktime(2000,1,1,i)).should eq 'おはようございます'
            }
        end
        specify 'afternoon' do
            (12...18).each{|i|
                @greeter.greet(Time.mktime(2000,1,1,i)).should eq 'こんにちは'
            }
        end
        specify 'night' do
            [*18...24]+[*0...5].each{|i|
                @greeter.greet(Time.mktime(2000,1,1,i)).should eq 'こんばんは'
            }
        end
    end

対象のメソッドに対して時刻オブジェクトを渡せばよいので、テストのための仕組みが必要無く、容易にテストできることがわかります。

このアプローチのメリット

このアプローチのメリットは、まず第一に依存が少なくシンプルであることです。このアプローチを選択した場合、 Greeter クラスは状態を持ちません。 greet メソッドの戻り値に影響を与えるのは引数だけなので、テストから渡す引数を変えることで様々なテストを行うことが簡単になります。引数だけで結果が決まること、テストが行いやすいことから、関数型プログラミングに近いスタイルと言うこともできるでしょう。

このアプローチのデメリット

このアプローチは引数の省略を使えない言語では使用できません。また、条件が増えるときに工夫が必要です。対象の複雑さが増すとき、例えばお題2ではロケールが増え、他にも能追加されると更に条件が増えていきますが、単純に引数を増やしていくアプローチでは限界が訪れます。リファクタリング『引数オブジェクトの導入』や関数型プログラミングの知見を生かして、コードの複雑性を上げない取り組みが必要になるでしょう。

このコードの改善できる点

重箱の隅をつつくならば、 Ruby 的には更に改善の余地があったり、ローカルタイムゾーンが Asia/Tokyo であることに依存しているコードなのですが、現時点で十分シンプルなコードです。ただし、テストの書き方には改善できる点があります。それは、テストメソッド内のループです。

(注: これから説明するのは、テスト容易性設計のアプローチとは関係なく、ユニットテストの書き方に関する一般論です)

[ポイント] Assertion Roulette

テストメソッド内のループは、Assertion Rouletteと呼ばれるテストの不吉な臭いなのです。Assertion Roulette について、先ほどのテストコードからテストメソッドを一つ持ってきて説明しましょう。

    specify 'morning' do
        (5...12).each{|i|
            @greeter.greet(Time.mktime(2000,1,1,i)).should eq 'おはようございます'
        }
    end

RSpec や Ruby の語法が使われているので Java 風に書き直すと、以下のようになるでしょう。ループの中にアサーションが書かれています。

    @Test
    public void testMorning() {
        Greeter greeter = new Greeter();
        for (i = 5; i < 12; i++) {
            assertThat(greeter.greet(Time.mktime(2000,1,1,i)), is("おはようございます"));
        }
    }

説明のために、ループを使わずに書く場合のコードに更に書き直します(発生するコードの重複は、 Assertion Roulette の説明の本質ではないので、一時的に目をつぶってください)。ループが展開されると、アサーションが縦に7行並んでいます。

    @Test
    public void testMorning() {
        Greeter greeter = new Greeter();
        assertThat(greeter.greet(Time.mktime(2000,1,1, 5)), is("おはようございます"));
        assertThat(greeter.greet(Time.mktime(2000,1,1, 6)), is("おはようございます"));
        assertThat(greeter.greet(Time.mktime(2000,1,1, 7)), is("おはようございます"));
        assertThat(greeter.greet(Time.mktime(2000,1,1, 8)), is("おはようございます"));
        assertThat(greeter.greet(Time.mktime(2000,1,1, 9)), is("おはようございます"));
        assertThat(greeter.greet(Time.mktime(2000,1,1,10)), is("おはようございます"));
        assertThat(greeter.greet(Time.mktime(2000,1,1,11)), is("おはようございます"));
    }

Assertion Roulette は、テストが上手くいっているときは何も問題ないのですが、テストが失敗したときに牙をむきます。このテストが落ちたときに、どのアサーションが失敗したのかがわかりにくいのです。(Assertion Roulette という名前は、シリンダーのどの穴に弾が入っているかわからないロシアンルーレットのイメージです)

テスト名からはどのアサーションが失敗したかは読み取れないので、テスト失敗時のスタックトレースから行番号を見つけるなどの方法で、何行目のアサーションが失敗したのかを読み取らなければなりません。テストを手元で実行しているのならまだ読み取りやすいですが、 CI サーバで実行されるときは、失敗したアサーションの推定が難しくなることもあるでしょう。

また、たとえば何らかの事情で4つめのアサーション (8時に対するアサーション) でテストが失敗したとします。この場合に、9時,10時、11時のアサーション結果はどうなったでしょうか。答えは「わからない」です。より正確に表現するなら「実行されなかったのでわからない」です。

一般的なテスティングフレームワークではアサーション失敗時に例外が発生し、そのテストメソッドの実行を打ち切ります。8時のアサーションが失敗した場合、その行以降のアサーションは実行されていないのです。8時だけが落ちるのか、9時以降も落ちるのか、実行されていないのでわかりません。
(ちなみに、 Perl のテスト文化に属するテスティングフレームワークはアサーションが失敗してもテストが続くので、事情がやや異なります)

ということで、縦に何行も並んだアサーションや、ループの中で書かれたアサーションには、以下のような問題点があることがわかりました

  • どのデータが原因でテストが失敗したかわかりにくい
  • テスト失敗以後のアサーションが行われない

Assertion Roulette を避けるためには テストメソッド内に制御構造はなるべく書かないと覚えておいてください。条件分岐はもちろんのこと、繰り返しも、やや不吉な臭いです。なるべく別の方法でテストの場合分け、組み合わせに対応させましょう。

[ポイント] 目指すのは「テストメソッド毎にアサーションひとつ」

Assertion Roulette を避けるためにも、目指したい状態はテストメソッド毎にアサーションひとつ(one assertion per test)です。テストメソッド毎のアサーションがひとつであれば、テストが落ちたときにどのアサーションが失敗したのかは自明です。また、テスト毎にアサーションをひとつにするように努力すると、テストのピントがはっきりします。多くのことをテストするひとつのテストメソッドがあるよりも、ひとつのことだけをテストする沢山のテストメソッドがあるほうが望ましい、と覚えておいてください。

[ポイント] カスタムアサーションを使う

テスティングフレームワークにデフォルトで提供されているアサーションだけを使うのも、 Assertion Roulette の原因になりがちです。比較的大きな、例えば検証対象のプロパティが沢山あるオブジェクトに対してデフォルトのアサーションだけを使用してテストメソッドを書くと、簡単に Assertion Roulette 状態に陥ってしまいます。このようなときは、カスタムアサーション(Custom Assertion)を作成しましょう。

カスタムアサーションとは、プロジェクト固有のオブジェクトに合わせて自分たちで作成したアサーションのことです。今回の Greeter では戻り値が文字列なのでカスタムアサーションを作成する余地はあまりありませんが、大きめのドメインオブジェクトの状態や属性を沢山持っているデータクラスの、ひとつひとつの属性に対してアサーションを書いていくのではなく、例えばデータクラスを丸ごと検証するアサーションを自分たちで作成するイメージです。上手く設計されたカスタムアサーションは、テスト毎のアサーション数を減らしつつ、テストコードの読みやすさや失敗時の情報を高めることが出来ます。

カスタムアサーションに関しては、JUnit ではJUnitのカスタムアサーションを簡単に実装できるcmtest、 RSpec では Custom matchers – RSpec Expectations、PHPUnit では PHPUnit の拡張なども読んでみてください。

[ポイント] シンプルなコードとテスト失敗時の情報のバランス

テストメソッドの外に制御構造を作れるような動的言語の場合は、テストメソッド内でループする代わりに、せめてテストメソッドの外でループする、という方法を選択する場合もあります。例えば、下記のような感じです。アサーションが沢山あるひとつのテストメソッドではなく、アサーションがひとつのテストメソッドが沢山ある状態をプログラミング的に作り出します。

    describe Greeter do
      before do
        @greeter = Greeter.new
      end
      context 'morning' do
        (5...12).each do |i|
          specify "at #{i} AM" do
            @greeter.greet(Time.mktime(2000,1,1,i)).should eq 'おはようございます'
          end
        end
      end
      # ...略...
    end

こうすることで、 Assertion Roulette 状態は脱することができます。アサーションが落ちた時に、どのテストが落ちたのか見分けを付けられるようにテスト名にデータをつなげているところがコツです。

[ポイント] やりすぎ禁物

とはいえ、たとえテストメソッドの外であれ、テストにループがあるのはやはり可読性を損なうものです。やりすぎてループが二重になったりすると、テストの可読性はもはや悪化します。プロダクトコードと同様にテストコードも重複が無い DRY (Don’t Repeat Yourself) な状態が重要ですが、やり過ぎは禁物です。

テストコードは意図を表現するシンプルなコードであることが重要です。Assertion Roulette を避けるコードとシンプルなコードが両立できないときは、シンプルなコードを選択することが大事です。何事もこだわりすぎは禁物です。

シンプルさだけでなくテスト失敗時の振る舞いも重要なので、目指したいのは Assertion Roulette 等の失敗時の情報不足を避けるテストコーディングをしつつ、シンプルなコードを求める姿勢です。

シンプルさと失敗時の情報粒度を保ちつつ、より意図を表現するテストを得るためには、次節の「パラメタライズドテスト」に進むことも考えてみてください。

[ポイント] パラメタライズドテスト(Parameterized Test)

「名前重要」というプログラミングの基本は、テストメソッド名にも当然適用されます。可読性の高いテストのためには、テストメソッド名が非常に重要です。

ただ、上記のテストコードのように、同じ振る舞いに対するデータ違いのテストに対して one assertion per test を心がけて Assertion Roulette 状態を避けるようなテストコードを書いていると、テストメソッド名の名付けに困るような状況がしばしば現れます。テストメソッド外にループを書くようなときも、テストの名前よりデータが重要、という状況であるといえます。

ほぼ同じテスト内容でデータだけを変えたテストメソッドを(列挙であれループであれ)書いているときに、テストメソッドにパラメータを渡せればいいのに、と感じることがあると思います。このようなときはパラメタライズドテスト (Parameterized Test)というキーワードでテスティングフレームワークの機能を探してみてください(パラメタライズドテストは「パラメータ化テスト」とも呼ばれています)。 Parameterized Test とは、まさに似たようなテストメソッドをテストデータだけ変えて複数件実行することができる手段で、多くの場合テスティングフレームワークの拡張機能として提供されています。

お使いのテスティングフレームワークで、ぜひパラメタライズドテストの書き方を調べてみてください。JUnit4 では Parameterized アノテーションTheories アノテーション、PHPUnit ではデータプロバイダ機能、RSpec ではrspec-parameterizedなどが使えます。JUnit4 でパラメタライズドテストを書く場合には、内部クラスを利用した入れ子構造のテストを組み合わせると効果的です。JUnit4 における入れ子のテストとパラメタライズドテストに関しては、詳しくは『JUnit 実践入門』を読んでみてください。

今回のテストコードを rspec-parameterized を使って書いてみると、例えば以下のようなテストコードになります。テストデータをテストメソッドから分離できるパラメタライズドテストの特徴が出ているのではないでしょうか。

    require 'rspec'
    require 'rspec-parameterized'
    require_relative './greeter'

    describe Greeter do
      def greet_at(anHour)
        greeter = Greeter.new
        greeter.greet(Time.mktime(2000, 1, 1, anHour))
      end

      where(:hour, :expected_greet) do
        [
         [0,  'こんばんは'],
         # 中略
         [4,  'こんばんは'],
         [5,  'おはようございます'],
         # 中略
         [11, 'おはようございます'],
         [12, 'こんにちは'],
         # 中略
         [17, 'こんにちは'],
         [18, 'こんばんは'],
         # 中略
         [23, 'こんばんは'],
        ]
      end

      with_them do
        it { expect(greet_at(hour)).to eq(expected_greet) }
      end
    end

なお、パラメタライズドテストをどう使うかは、今回の問題のゲスト解答者、後藤さんのコードにも出てきますので、ぜひ読んでみてください。

組み込みクラス/メソッド/関数に介入する

さて、次のアプローチに行きましょう。
今回最も多くの方が選択したのが、この「組み込みクラスに介入する」アプローチでした。

具体的には、多くの言語が備えている「現在時刻を返すメソッド」を動的に差し替え、現在時刻を取得しようとするとテストの中から設定した時刻が返されるようにする、というアプローチです。このアプローチを選択できるのは主に動的言語ですが、きんきんさんはC# 4.0 でMolesというライブラリを使用して標準クラスに介入しています。やりようによっては静的言語でもこのアプローチを行えるようですね。

ではこのアプローチのテストコードの代表例として、ynakさんのテストコード(RSpec)を見てみましょう。(プロダクトコードは、前節のcielさんのコードの引数なしバージョンとほぼ同じと考えてください)

    describe Greeting do
        subject do
            greeter = Greeting.new
            greeter.greet()
        end
        context 'at morning' do
            it do
                Time.stub(:now).and_return(Time.new(2013,10,12,5))
                expect(subject).to eq("おはようございます")
            end
        end

        context 'at noon' do
            it do
                Time.stub(:now).and_return(Time.new(2013,10,12,12))
                expect(subject).to eq("こんにちは")
            end
        end

        context ' at night' do
            it do
                Time.stub(:now).and_return(Time.new(2013,10,12,18))
                expect(subject).to eq("こんばんは")
            end
        end
    end

前の節のcielさんのコードにも出てきましたが、Ruby はTime.nowというクラスメソッドで現在時刻を返します。

ynakさんのテストコードでは、テスティングフレームワークRSpecのstub機能を使用して、テスト毎に望む値を返すように Time.nowを偽物に置き換えています。たとえばTime.stub(:now).and_return(Time.new(2013,10,12,5))としておくと、Time.nowは現在時刻の代わりにTime.new(2013,10,12,5)の結果を返すというわけです。

[ポイント] テストダブル

再現性のあるテストのために本物のオブジェクトを置き換えたテスト用の偽物オブジェクトのことをテストダブル(Test Double)といいます(Doubleとは、影武者のようなニュアンスだと考えてください)。テストダブルには様々な種類がありますが、今回のお題の解答者の皆様はスタブモックを使うことが多いようです。

テストダブルは、ユニットテストの依存を減らし、再現性を上げるための非常に強力なテクニックです。強力すぎるゆえに、多用してしまいやすい、脆いテスト(後述)の原因になりやすいなどの問題もありますが、ユニットテストを記述する際に必須の技術とも言えますので、ぜひマスターしましょう。

テストダブル、スタブ、モックなどのより詳しい分類や説明は、xUnit Test Patterns WikiのTest Doubleのページや、goyokiさんのエントリを読んでみてください。

このアプローチのメリット

このアプローチのメリットは、前節のアプローチとも共通ですが、対象クラスにテストのための仕組みが不要なことです(ということは、今後紹介するアプローチにはテストのための仕組み/コードが多かれ少なかれ登場するということです)。

テストのためのコードは本来は不要なコードであるという主張は、ある程度は真実です。動的言語の力やモックライブラリの能力を使い、プロダクトコードは特にテストへの考慮を行わずともテストを書くことができるというのは、実装の後からでも、ほかの人の書いたコードに対しても、容易にテストを書けることを意味します。

このアプローチのデメリット

このアプローチのデメリットは、強力さの裏返しです。力には責任が伴います。組み込みクラス/メソッド/関数の動きを変えるというのは、影響の大きい変更です。

例えば現在時刻を返すメソッドTime.nowを差し替えるとき、影響を受けるのは直接的あるいは間接的にTime.nowを使用しているすべての部分です。思ったよりも影響範囲が広いな、と感じる方が多いのではないでしょうか。標準ライブラリは有名人であるため、一種のグローバル汚染が発生してしまうのです。強力さはリスクと隣り合わせです。思わぬ副作用に注意しなければなりません。

ゆえに、このアプローチを使う場合には、テスト後の始末を忘れずに行うことが重要です。テストの中で振る舞いを変えた標準クラス/メソッド/関数を元に戻しておかないと、他のテストにも影響が残ります。

[ポイント] 良いユニットテストは独立(Independent)していなければならない

あるテストの内容が他のテストの結果に作用するとき、それらのテストには依存関係があります。そして、良いユニットテストはテスト間に依存関係を持ってはならず、互いに独立(Independent)していなければなりません。

テスト間がstatic領域やグローバル変数、Singleton等を介してつながってしまっているようなテストはIndependentではありません。

Independentでないユニットテストは、特定の順番でないと通らないテストや、テスト群をまとめて動かすと通るのに、一つだけではなぜか通らない(またはその逆)テストのような、Erratic Test(不安定なテスト)を生み出します。特にテストが裏で繋がって作用しあってしまうことを、Interacting Testsといいます。

[ポイント] 後始末を忘れずに行い、テストを独立させる

テストを互いに独立したものにするために、テストで差し替えた振る舞いは、各テスト後に元に戻しましょう。なお、テスティングフレームワークに同梱されたモックライブラリの場合は、各テストの終了時に差し替えた振る舞いを元に戻してくれるものもあります。たとえば今回ynakさんが使用しているRSpecのmock/stub機能は、差し替えた各テスト後に振る舞いを元に戻します。

フレームワークに頼らず自前で振る舞いを差し替えた場合には、各テストメソッドの終了時に呼び出されるフック(多くの場合 tearDownまたはafter等の名前がついています)で、差し替えた振る舞いを元に戻しましょう。

今回の解答者の中では、ishiducaさんがJavaScriptでDateクラスに対して自前のスタブを作成し、teardownで元に戻しています。

    // ...略...
    q.module('.greet', {
        setup: function () {
            var stub = this.stub = {}
            this.getUTCHours = Date.prototype.getUTCHours
            this.getUTCMinutes = Date.prototype.getUTCMinutes
            Date.prototype.getUTCHours   = function () { return stub.hour }
            Date.prototype.getUTCMinutes = function () { return stub.min }
        }
      , teardown: function () {
            Date.prototype.getUTCHours = this.getUTCHours
            Date.prototype.getUTCMinutes = this.getUTCMinutes
            this.stub = null
        }
    })

    q.test('.greet は時刻によって適切な挨拶を返すか', function (t) {
        var stub = this.stub
        var g = new Greet
        function subt (gmtHour, min, result) {
            stub.hour = gmtHour
            stub.min  = min
            t.is(g.greet(), result)
        }
        subt(0,  0, 'おはようございます') //  9:00
        subt(2, 59, 'おはようございます') // 11:59
        subt(3,  0, 'こんにちは') // 12:00
        subt(8, 59, 'こんにちは') // 17:59
        subt(9,  0, 'こんばんは') // 18:00
        subt(15, 0, 'こんばんは') // 24:00
        subt(19, 0, 'こんばんは') //  4:00
    })
    // ...略...

ちなみにishiducaさんが使用しているのはPerl系のテスト文化を継いだテスティングフレームワークQUnitなので、縦に並んでいるアサーションの途中が失敗しても先に進みます。ただし、t.isアサーションメッセージ引数を与えないと、結局はAssertion Rouletteになってしまう点に注意しましょう。

対象クラスだけが使う組み込みクラスに介入する

「組み込みクラスに介入する」アプローチに似ていますが、antimon2さんは一風変わったアプローチでテストに取り組んでいます。まずはプロダクトコード(Ruby)を見てみましょう。特に特筆する点はなく、シンプルで穏当なコードになっています。

    class Greeter
      def greet
        time = Time.now
        hms = time.hour * 10000 + time.min * 100 + time.sec
        case hms
        when 50000...120000
          "おはようございます"
        when 120000...180000
          "こんにちは"
        else
          "こんばんは"
        end
      end
    end

では、テストコード(test::unit)を見てみましょう。

    require 'test/unit'

    class Greeter
      # トップレベルのTimeクラスを継承して動作変更
      class Time < ::Time
        @@now = nil

        def initialize *args
          if !@@now.nil? && args.empty?
            args = [@@now.year, @@now.mon, @@now.day, @@now.hour, @@now.min, @@now.sec]
          end
          super *args
        end

        class << self
          def now= time
            @@now = time
          end

          def now
            @@now || new
          end
        end
      end
    end

    class GreeterTest < Test::Unit::TestCase
      // ...略...
      def test_greet_afternoon_1759
        Greeter::Time.now = Time.local(2013, 10, 8, 17, 59, 59)
        greeter = Greeter.new
        result = greeter.greet
        assert_equal("こんにちは", result)
      end

      def test_greet_evening_1800
        Greeter::Time.now = Time.local(2013, 10, 8, 18, 0, 0)
        greeter = Greeter.new
        result = greeter.greet
        assert_equal("こんばんは", result)
      end
      // ...略...
    end

テストコードの最初でGreeterクラスの定義を開き、Greeterから見えるTimeを書き換えています。こうすることで、組み込みクラス Time の動作を書き換える影響範囲をGreeterにとどめているわけですね。テストの影響範囲を狭める仕組みとして、面白い発想だと思います。

このアプローチのメリット

このアプローチのメリットは、プロダクトコードにテストのためのコードが不要で、なおかつ「組み込みクラスに介入する」アプローチより影響範囲が小さいことです。

このアプローチのデメリット

このアプローチのデメリットは「組み込みクラスに介入する」アプローチのデメリットと同様です。強力さにはリスクが伴うので利用には注意し、後始末を忘れずに行いましょう。また当然ながら、プログラミング言語によっては選択できないアプローチであることも明記しておかなければなりません。

このコードの改善できる点

このテストコードで気になるのはテストメソッド間のコード重複が多いこと、そして現在時刻の設定コードが各メソッドでむき出しになっており、ノイズになっていることです。ここから、次の改善ポイント「テストコードのノイズを減らす」が導き出されます。

[ポイント] テストコードのノイズを減らす

テストコードの中に重複が増えてきたり、テスト条件の組み立て部分のコードが複雑になってくると、少し見ただけでは何をテストしているのか読み取りにくくなってきます。テストメソッドの中には、他のテストメソッドと違う点だけを、違いが際立つように書きたいものです。

例えば今回のコードでは共通化できる部分は各テストメソッド開始時のフックメソッドsetupに抽出し、現在時刻設定部分は、テスト毎に違う部分だけを書けば良いようにこちらもメソッドに抽出します。すると、例えば以下のように書けるようになります。

    class GreeterTest < Test::Unit::TestCase
      def setup
        @greeter = Greeter.new
      end

      // ...略...
      def test_greet_afternoon_1759
        set_current_time(17, 59, 59)
        assert_equal("こんにちは", @greeter.greet)
      end

      def test_greet_evening_1800
        set_current_time(18, 0, 0)
        assert_equal("こんばんは", @greeter.greet)
      end
      // ...略...

      private

      def set_current_time(hour, minute, second)
        Greeter::Time.now = Time.local(2013, 10, 8, hour, minute, second)
      end
    end

テスト毎に違う部分が際立つようになり、テストの可読性が上がったことがわかるのではないでしょうか。ノイズ減らしはテストコードのリファクタリングとして手軽に行える第一歩ですので、ぜひ取り組んでみてください。

対象クラスのテスト用サブクラスをテスト内で作成 (Test-Specific Subclass)

次のアプローチは、対象クラスの振る舞いを一部テスト内で上書きするアプローチです。現在時刻を取得する部分をメソッドにしておいて、そのメソッドだけテストからオーバーライドするというわけです。テスト対象の一部分をテスト用のサブクラスでオーバーライドするアプローチは、Test-Specific Subclassという名前もついているスタンダードな手法です。

解答された方の中では、kencharosさんがこのアプローチを採用しています。プロダクトコード(Java)を見てみましょう。

    public class Greeter {
      private static final int MORNING = 50000;
      private static final int NOON = 120000;
      private static final int NIGHT = 180000;

      protected Calendar current() {
        return Calendar.getInstance(TimeZone.getTimeZone("Asia/Tokyo"));
      }

      public String greet() {
        Calendar cal = current();
        int hh = cal.get(Calendar.HOUR_OF_DAY);
        int mm = cal.get(Calendar.MINUTE);
        int ss = cal.get(Calendar.SECOND);
        int time = hh * 10000 + mm * 100 + ss;
        if (MORNING <= time && time < NOON) {
          return "おはようございます";
        } else if (NOON <= time && time < NIGHT) {
          return "こんにちわ";
        } else {
          return "こんばんわ";
        }
      }
    }

kencharosさんのコードではcurrentメソッドがオーバーライド対象のメソッドですね。このメソッドだけオーバーライドしたインスタンスをテスト内で作成し、テストに使用するという方針です。

ではテストコード(JUnit 4.4)を見てみましょう。

    public class GreeterTest {
      private Greeter setTime(final int hh, final int mm, final int ss) {
        return new Greeter() {
          protected Calendar current() {
            Calendar cal = Calendar.getInstance();
            cal.set(Calendar.HOUR_OF_DAY, hh);
            cal.set(Calendar.MINUTE, mm);
            cal.set(Calendar.SECOND, ss);
            return cal;
          }
        };
      }
      @Test
      public void testMorningStert() {
        Greeter greeter = setTime(5,0,0);
        assertThat(greeter.greet(), is("おはようございます"));
      }
      @Test
      public void testMorningEnd() {
        Greeter greeter = setTime(11,59,59);
        assertThat(greeter.greet(), is("おはようございます"));
      }
      // ...以下略...

Greeterクラスを継承してテスト用のインスタンスを返しているのはsetTimeメソッドですね。各テストメソッドから時、分、秒を渡し、再現性のあるテストを書けるように工夫しています。

kencharosさんのテストコードの良い所は、各テストメソッドにノイズが少ないことです。各テストメソッドは準備、実行、検証を2行にまとめてあります。煩雑なコードになりがちな時刻の制御がメソッドに切りだされているので、各テストメソッドにはデータだけを書けばよく、読みやすいテストコードになっています。

このアプローチのメリット

このアプローチのメリットは、単純であることです。オブジェクト指向言語であれば動的であれ静的であれ選択できるアプローチであり、テスティングフレームワークにモック機能等がなくともテストを行えます。テストのために振る舞いが上書きされた部分はテストクラスの中に単純に書かれているため、見通しが立てやすいのもメリットといえそうです。

このアプローチのデメリット

このアプローチのデメリットは、テスト容易性のための仕組みがプロダクトコード側に必要な点です。

テストからプロダクトコードの一部をオーバーライドするには、テストコードはプロダクトコードの中身まで知っていなければなりません。このような状態は、結合度の高い状態であるといえます。テスト対象の実装が変わったら、テストコード側も忘れずについて行かなければなりません。

また、テストコードにプロダクトコードの構造の知識が一部流出しているので、テストが「外部から見た振る舞いの検証」になりきれていません。つまり、仕様のテストではなく、実装のテストになってしまっているのです。

[ポイント] Fragile Test (脆いテスト)

仕様は変わっていないのに、実装が変わったら落ちてしまうようなテストは、実装の知識がテストコードに漏れ出しているときに発生しやすくなります。つまり、不用意に実装に依存しているテストである可能性があります。このように落ちなくとも良いタイミングで落ちるテストをFragile Test (脆いテスト)といいます。不可解なタイミングで落ちるテストは、実装のテストになってしまっていないか注意しましょう。

このコードの改善できる点

かなりきれいな部類のテストコードなのですが、あえて改善点を挙げるならば、setTimeという名前からGreeterのサブクラスのインスタンスを返すことが想像しにくいので、その点が改善されればさらに読みやすいテストコードになると思います。さらに重箱の隅をつつくならば、テストメソッド名の綴り間違い等を直したいところです。ここから、もう一つの改善点「日本語テストメソッド」が導き出されます。

[ポイント] 日本語テストメソッド

日本語テストメソッドとは日本語を使っても問題ない場合、日本語でテストメソッドを書いても良いのではないかという考え方です。ぎこちない英語による記述をコメントで補うくらいなら、最初から日本語で書いてしまいましょう、とも言い換えられます。英語で書くのは難しかった細かいニュアンス等も、母語の日本語であれば書けるのではないでしょうか。

    @Test
    public void 朝の挨拶開始時刻は午前5時から() {
      Greeter greeter = setTime(5,0,0);
      assertThat(greeter.greet(), is("おはようございます"));
    }
    @Test
    public void 朝の挨拶終了時刻は午前11時59分59秒まで() {
      Greeter greeter = setTime(11,59,59);
      assertThat(greeter.greet(), is("おはようございます"));
    }

日本語テストメソッドにはもう一つの利点があります。日本語は英数字や記号だらけのコードや出力の中でひときわ目立つのです。

テストメソッドの中身をわざと失敗するように書き直して、失敗時の出力を見てみましょう。

    JUnit version 4.11
    ...E...E
    Time: 0.013
    There were 2 failures:

    1) 朝の挨拶終了時刻は午前11時59分59秒まで(GreeterTest)
    java.lang.AssertionError:
    Expected: is "おそようございます"
         but: was "おはようございます"
        at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
        at org.junit.Assert.assertThat(Assert.java:865)
        at org.junit.Assert.assertThat(Assert.java:832)
        at GreeterTest.朝の挨拶終了時刻は午前11時59分59秒まで(GreeterTest.java:27)

    2) 朝の挨拶開始時刻は午前5時から(GreeterTest)
    java.lang.AssertionError:
    Expected: is "おはよう"
         but: was "おはようございます"
        at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
        at org.junit.Assert.assertThat(Assert.java:865)
        at org.junit.Assert.assertThat(Assert.java:832)
        at GreeterTest.朝の挨拶開始時刻は午前5時から(GreeterTest.java:22)

Assertion Rouletteの説明のところで「どのアサーションが落ちたのかわかりにくい」という話をしましたが、テスト名もきちんと付けないと、結局どのテストが落ちたのかコードを読んで調べなければならなくなります。テスト名が日本語で適切に書いてあれば、落ちたテストの出力を見るだけで、どのようなテストが落ちたのか分かるようになります。条件に合う場合は、ぜひ日本語テストメソッドを検討してみてください。

日本語テストメソッドに関する議論はテストメソッドを日本語で書くことについて – Togetterや、日本語テストメソッドについて等で読むことができます。

テストを対象クラスのサブクラスにしてオーバーライド(Self Shunt)

前節のアプローチではテスト対象クラスのサブクラスをテスト内で作成していました。この考えを更に進めると、テスト対象をテスト可能にするためにサブクラスを作るのであれば、いっそのことテストクラス自体をテスト対象クラスのサブクラスにしてしまえば良いのではないだろうか、というアイデアにたどり着きます。このアプローチはSelf Shuntという名前も付いている、実は由緒正しいテクニックなのです。

今回はanagotanさんがSelf Shuntアプローチでお題を解いてくださいました。解答者の中にSelf Shuntを使う人が出てくるとは思っていないかったので、これにはかなり驚きました。ではまずanagotanさんのコード(Java)を見てみましょう。

    public class Greeter{

        // 現在時刻を返す
        public Date getDate(){
            return new Date();
        }

        public String greet(){
            SimpleDateFormat df=new SimpleDateFormat("HH");
            int now=Integer.parseInt(df.format(getDate()));
            if(5<=now && now<12){
                return "おはようございます";
            }else if(12<=now && now<18){
                return "こんにちは";
            }else{
                return "こんばんわ";
            }
        }
    }

次にSelf Shuntテクニックが使われているテストコード(JUnit4)を見てみましょう。

    public class GreeterTest extends Greeter{
        // staticにしておく
        private static Date date;

        @Override
        public Date getDate(){
            // テスト用にオーバーライドする
            return date;
        }

        @Test
        public void test(){
            testWithHour(0,0,0,"こんばんわ");
            testWithHour(4,59,59,"こんばんわ");
            testWithHour(5,0,0,"おはようございます");
            testWithHour(8,0,0,"おはようございます");
            // ...以下略...
        }

        private void testWithHour(int hour,int minute,int second,String answer){
            Calendar now=Calendar.getInstance();
            //テスト用の時間をセット
            now.set(Calendar.HOUR_OF_DAY, hour); 
            now.set(Calendar.MINUTE, minute); 
            now.set(Calendar.SECOND, second); 
            date=now.getTime();
            Assert.assertEquals(new GreeterTest().greet(),answer);
        }
    }

GreeterTestがGreeterを継承できるのは、JUnit4がテスト用の親クラスを持たないからですね。特定の親クラスを継承せずともテストを書けるテスティングフレームワークであれば、Self Shuntアプローチが可能です。

このアプローチのメリット

このアプローチのメリットは、テストクラス自身がテスト対象クラスを継承するので、テストから対象クラスの内部状態にアクセスしやすくなることです。テストクラスがテスト対象クラスのサブクラスなので、対象の任意のメソッドをオーバーライドしたり、テスト対象内部のインスタンス変数の状態を見たりするテストを書きやすくなります。

このアプローチのデメリット

このアプローチのデメリットは基本的には「対象クラスのテスト用サブクラスをテスト内で作成」アプローチと同様ですが、Self Shunt はメリットとデメリットが更にハッキリしています。テストクラス自身がテスト対象クラスを継承するので、テストコードとプロダクトコードの結合度がさらに高くなるのです。ゆえに、Fragile Test (脆いテスト)に陥る危険性も高まります。

また、プログラミング言語やテスティングフレームワークによってはテスト対象を継承できないので、そもそもSelf Shuntアプローチを選択できない点にも注意が必要ですね。

このコードの改善できる点

このコードには、大きく三つの改善できる点があります。

  • Assertion Rouletteになっている
  • テスト内のdate変数がstaticである必要はない
  • Greeter#getDateがpublicである必要はない

まず、ひとつのテストメソッドの中にアサーション(を内包したメソッド)が縦に並んでいて、典型的なAssertion Rouletteになっているので、テストデータ毎にテストメソッドを分けましょう。また、テストのバリエーションをループ等で表現したい場合は、Javaはテスト用の制御構造をテストメソッド外に書けるような柔軟性は持っていないので、一足飛びに理想状態、パラメタライズドテスト(Parameterized Test)にするのが良いでしょう。

[ポイント]staticを避け、テストメソッド間の依存関係を断つ

次に、テスト内のdate変数がstaticである必要もありません。staticにすると、テストメソッド間に暗黙の依存関係を生んでしまいます。暗黙の依存関係は、ユニットテストは互いに独立している(Independent)べきという原則に反しており、Interacting Testsの原因になります。

テスティングフレームワークの多くは、テストメソッド毎にテストクラスのインスタンス化を行います。つまりstatic領域はテストメソッド間で共有されますが、インスタンス変数は共有されません。テストに使うデータは毎回生成/破棄されるインスタンス変数を使いましょう。

テスティングフレームワークがなぜテストメソッド毎にテストクラスのインスタンス化を行うのかは、JUnit新インスタンスを読んでみてください。

[ポイント] テストだけに使う部分の可視性を下げる

最後に、Greeter#getDateがpublicである必要はありません。Self Shuntを使っているのでpublicでなくともオーバーライドできますし、getDateメソッドはGreeterクラスの責務とは直接は関係がありません。テストのためだけに使う部分は、なるべく可視性を下げておきましょう

テストからオーバーライドする対象の可視性をどうするかは、Javaの場合はprotectedやdefault(package private)と呼ばれる可視性にしておくのが妥当でしょう。privateメソッドに対してリフレクションを使用して介入する手もありますが、これはさらに不必要に実装に依存してしまうことにもなるので、かなり悩ましいところです。ここから、次のポイントにつながります。

[ポイント]privateメソッドとテスト

ユニットテストを書いていると、privateメソッドをどうするか、という問題によく出会います。リフレクション等を使って private メソッドをテストしたい、もしくはテストのためにprivateメソッドを呼び出したい場合は、なにかおかしいことの予兆と考えた方が良いでしょう。privateに触れたくなるのは、テスト対象が責務を持ちすぎていることのサインです。無理にそのままリフレクションを使うのではなく、リファクタリングによって解決した方が、良い結果を生むことが多いでしょう。プライベートメソッドとテストの議論に関しては、プライベートメソッドのユニットテストは書かないもの?- QA@ITも読んでみてください。

対象クラスの特定メソッド定義をテストで書き換える

対象クラスを継承するアプローチのバリエーションとして、動的言語の特性を活用して、対象クラス定義へ介入するというアプローチもあります。言語本来の機能やテスティングフレームワークのテストダブル機能を使い、テスト対象クラスの定義をテスト毎に少し書き換えて実行するというものです。

解答者の皆様の中ではushiboyさんがこのアプローチで問題を解いています。プロダクトコード(JavaScript)を見てみましょう。

    // ...略...
    /**
     * 現在時刻の挨拶を返す
     *
     * @return {String} 挨拶
     */
    Greeter.prototype.greet = function() {
        // 現在時刻
        var now = this.getNow(),
            // 午前の開始タイムスタンプ
            amForm = (new Date(now)).setHours(0, 0, 0, 0) + 5 * 60 * 60 * 1000,
            // 午後の開始タイムスタンプ
            pmFrom = amForm + 7 * 60 * 60 * 1000,
            // 夕方の開始タイムスタンプ
            nightFrom = pmFrom + 6 * 60 * 60 * 1000;
        if (now >= amForm && now < pmFrom) {
            return this.messages.am;
        } else if (now >= pmFrom && now < nightFrom) {
            return this.messages.pm;
        }
        return this.messages.night;
    };

    /**
     * 現在時刻をタイムスタンプで返す
     *
     * @private
     * @return {Number} 現在時刻
     */
    Greeter.prototype.getNow = function() {
        return Date.now();
    };
    // ...略...

getNowメソッドが現在時刻を取得するメソッドですね。次にテストコード(Jasmine)を見てみましょう。

    describe('#greet', function() {
        it('朝の場合おはようございますを返す', function() {
            var date = new Date();
            spyOn(Greeter.prototype, 'getNow')
            .andReturn(date.setHours(5, 0, 0, 0))
            .andReturn(date.setHours(11, 59, 59, 999));
            var greeter = new Greeter();
            expect(greeter.greet()).toBe('おはようございます'); 
            expect(greeter.greet()).toBe('おはようございます'); 
        });
        it('昼の場合こんにちはを返す', function() {
            var date = new Date();
            spyOn(Greeter.prototype, 'getNow')
            .andReturn(date.setHours(12, 0, 0, 0))
            .andReturn(date.setHours(17, 59, 59, 999));
            var greeter = new Greeter();
            expect(greeter.greet()).toBe('こんにちは'); 
            expect(greeter.greet()).toBe('こんにちは'); 
        });
        // ...以下略...
    });

Jasmineのテストダブル機能spyOnメソッドを使用してGreeterのインスタンス化前にGreeter#getNowの定義に介入し、テストで指定されたDateを返すように仕向けています。「対象クラスのテスト用サブクラスをテスト内で作成」アプローチの動的言語版という位置づけと言ってもいいかもしれません。

このテストコードは、テストメソッドの中にアサーションが2行ずつあります。なぜ2行あるのか少し見ただけでは意図が読み取りづらいかもしれませんが、実はモックライブラリの一般的な機能と関係があります。

    spyOn(Greeter.prototype, 'getNow')
      .andReturn(date.setHours(12, 0, 0, 0))
      .andReturn(date.setHours(17, 59, 59, 999));

という部分は、Greeter#getNowというメソッドの定義を、getNowが1回目に呼び出されたときはdate.setHours(12, 0, 0, 0)を、2回目に呼び出されたときはdate.setHours(17, 59, 59, 999)を返すように書き換えています。メソッドの戻り値を1回目と2回目で変えるようにしているのですね。呼び出し毎に戻り値を変えられる機能は、モックライブラリの強力な部分でもあります。

このアプローチのメリット

このアプローチのメリットは、サブクラスを作る必要がないなど、継承系のアプローチに比べると手軽であることです。また、これは継承系アプローチでも無名内部クラス等を使えば行えることですが、テストメソッド毎に振る舞いを変更できます。

このアプローチのデメリット

このアプローチのデメリットは基本的に「対象クラスのテスト用サブクラスをテスト内で作成」と同様です。さらに、クラス定義の実行時書き換え系の技術を使っているので、テスト後に定義を元に戻すような後始末が必要です。テスティングフレームワークやモックライブラリが定義の復元を自動的に行なってくれるか調べ、行わない場合は自前で元に戻す作業が必要でしょう。

また、直接はこのアプローチのデメリットではありませんが、今回のコードはオーバーライド対象のメソッド名’getNow’が文字列で記述されています。変更対象のメソッドを文字列で記述するスタイルのライブラリは、テスト対象の変更に追随させることを忘れやすい点に注意してください。

このコードの改善できる点

今回の対象に関してモックライブラリの呼び出し毎の戻り値変更機能を使うのは、少々やり過ぎかもしれません。テストコードの中に重複やノイズが発生する原因にもなっています。テストコードのノイズを減らすリファクタリングを行い、朝の場合、昼の場合、夜の場合等のサブコンテキストに分けて、それぞれの中で一件ずつのアサーションを行うテストメソッドが複数ある状態を作れるのではないでしょうか。

現在時刻へのアクセスを行うインターフェイスを抽出

さて、本稿で詳解する最後のアプローチが「現在時刻へのアクセスを行うインターフェイスを抽出する」というものです。これまでのアプローチは基本的にプロダクトコードがひとつ、テストコードが一つのクラスから構成されていました。このアプローチでは、Greeter クラスは自分で現在時刻を取得するのではなく、現在時刻を取得するために別のオブジェクト(コラボレータ)とやりとりを行います。

このアプローチの代表的なコードとして、tdoiさんのコードを見てみましょう。

まずtdoiさんは現在時刻へのアクセスを行うインターフェイスをEnvironmentと名付け、お題1に必要十分なgetHourメソッドだけを定義しています。

    public interface Environment {
        public int getHour();
    }

さらに、Environmentインターフェイスと対になるデフォルト実装DefaultEnvironmentを作成しています。この DefaultEnvironmentが、現在時刻から時間部分を返す実装になっているわけですね。

    import java.util.Calendar;

    public class DefaultEnvironment implements Environment {
        public int getHour() {
            Calendar calendar = Calendar.getInstance();
            return calendar.get(Calendar.HOUR_OF_DAY);
        }
    }

このアプローチでは、Greeterクラスは現在時刻を知りません。だれが現在時刻を知っているのかだけを知っています。つまり、 Environmentインターフェイスを実装したコラボレータから現在時刻を取得して挨拶を返すという責務を担います。

    public class Greeter {

        Environment environment = new DefaultEnvironment();

        public void setEnvironment(Environment environment) {
            this.environment = environment;
        }

        public String greet() {
            int hour = this.environment.getHour();
            if (0 <= hour && hour < 12) {
                return "おはようございます";
            } else if (12 <= hour && hour < 18) {
                return "こんにちは";
            } else {
                return "こんばんは";
            }
        }
    }

インスタンス変数environmentにはデフォルトでDefaultEnvironmentクラスのインスタンスが設定されているので、テスト用に差し替えられることがなければGreeterはDefaultEnvironmentから現在時刻を取得して、現在時刻に合わせた挨拶を返すというわけですね。

ではテストコードを見てみましょう。

    import static org.junit.Assert.*;
    import org.junit.*;
    import org.jmock.Mockery;
    import org.jmock.Expectations;

    public class GreeterTest {

        Mockery context = new Mockery();

        Greeter greeter = null;
        Environment environment = null;

        @Before
        public void setUp() {
            environment = context.mock(Environment.class);
            greeter = new Greeter();
            greeter.setEnvironment(environment);
        }

        @Test
        public void responseShouldBeOhayogozaimasuAt0500() {
            responseShouldBeExpectedWord(5, "おはようございます");
        }

        @Test
        public void responseShouldBeKonnichiwaAt1200() {
            responseShouldBeExpectedWord(12, "こんにちは");
        }

        @Test
        public void responseShouldBeKonbanwaAt1800() {
            responseShouldBeExpectedWord(18, "こんばんは");
        }

        private void responseShouldBeExpectedWord(int hour, String expected) {
            final int currentHour = hour;
            context.checking(new Expectations() {{
                oneOf (environment).getHour(); will (returnValue(currentHour));
            }});

            String response = greeter.greet();

            context.assertIsSatisfied();
            assertEquals(expected, response);       
        }
    }

ここでテストに使われているライブラリは『実践テスト駆動開発』の著者二人(Steve Freeman, Nat Pryce)も開発に関わっているjMock2というモックライブラリです(実はこの二人はモックオブジェクトの発明者でもあります)。

テストの前準備部分 (@Beforeアノテーションの付いたメソッドはテストメソッド毎に実行されます) で context.mock(Environment.class)を呼び出し、Environmentインターフェイスのテスト用の偽物(モックオブジェクト)を作成し、GreeterのsetEnvironmentメソッドを使用してGreeterが使う Environmentインスタンスをあらかじめテスト用のモックオブジェクトに差し替えておきます。

各テストではモックオブジェクトが返す値をそれぞれ設定し、時刻に対応した返答があるかどうかをテストしているわけですね。

プロダクトコードもテストコードも適切な粒度のクラス/メソッドに切り分けられていて、バランスが良く穏当なコードになっていると思います。かなり好印象です。

なお、ゲスト解答者の後藤さんも、「現在時刻へのアクセスを行うインターフェイスを抽出する」アプローチを使っています。後藤さんはどのような設計を行ったのか、ぜひ読んでみてください。

このコードの改善できる点

一見してわかるのは、テスト件数がやや足りない、ということです。 Assertion Rouletteに注意しながら、足りないテストを足していきましょう。また、現時点で十分可読性の高いテストコードになっていますが、日本語テストメソッドを使えばさらに可読性の高いコードになるでしょう。

また、jMock2は強力なものの、ややトリッキーな記述を必要とするモックライブラリなので、他のモックライブラリと比較検討した上で導入するのが良いでしょう。例えばJavaで同じ「現在時刻へのアクセスを行うインターフェイスを抽出する」アプローチで解いた Touchuさんはmockitoというライブラリを使用しています。モックライブラリの記述性はテストの可読性に直接影響しますので、ライブラリの選択は非常に重要です。

このアプローチのメリット

このアプローチのメリットは、実はテストのための仕組みを導入しているわけではない、という点です。テクニックではなく設計変更によって、裏口(実装上書き)ではなく表玄関(コラボレータの差し替え)から、テストを行えるようになっています。

テストはテスト対象の中身を書き換えるのではなく、テスト対象とやり取りを行うコラボレータをすり替えています。テスト対象は普段と変わらず Environmentオブジェクトと話しているだけですから、そこにはテストのためにテスト対象を上書きするという強引さはありません。テスト対象の内部実装ではなく外部から見た振る舞いに応じたテストができている、というところがポイントです。

Environmentの責務は現在時刻の時間部分をintで返すことです。そして、Greeterの責務はEnvironmentに現在時刻の数値を聞いて、その値に応じて挨拶の内容を返すことです。責務がはっきりしています。登場人物は二人に増えましたが、やり取りする情報はint だけになりました。

DefaultEnvironmentはDefaultEnvironmentでテストを行う必要がありますが、こちらは現在時刻の時部分を返すことだけをテストすればよいので、テストの難易度は下がります。責務が単純になるので、特に本稿の他のアプローチを使わずともテストが書けるのではないでしょうか。

[ポイント] 外部環境との界面にインターフェイスを作成し、テストダブルで置き換える

実は現在時刻に関する設計判断は、テストだけの問題ではありません。その時刻は、どう使われるのでしょうか。仕様に「現在時刻を保存する」などとある場合に、本当にそれは厳密な現在時刻を必要としているのかどうか、そして、各所でTime.now等で取得するように実装すべきなのかどうか、考える必要があります。処理のタイミングによって現在時刻はもちろん変わります。処理に時間がかかったときに、現在時刻に依存したロジックがある場合には動作不良の元になることがありますし、後からデータを絞り込む際に時刻の完全一致で調べることが出来なくなります。

例えばWebシステムの場合には、仕様にある「現在時刻」は実はリクエストのタイムスタンプのことであったことが後から分かる、ということもあるでしょう。コードのあちこちに現在時刻を取得するメソッドが散らばっている場合には、それらをひとつひとつ検証していかなくてはなりません。tdoiさんのようにインターフェイスに抽出してあれば、リクエスト時刻を返すEnvironment実装を作成して使えば良くなるので、変更箇所はクラスの追加と利用設定部分だけで済むことになります。

私はコードの世界、オブジェクト達の世界から外部環境にアクセスする界面の部分にインターフェイスを作成し、テストダブルで置き換えられるようにするという設計判断をよく行います。現在時刻も外部との界面だと考えています。現在時刻は、最近では強力なライブラリの出現で制御しやすくなってはきましたが、その強力さゆえに副作用もあるので、やはり手強い部分です。ほかにも外部ネットワークに依存した部分、たとえば外部Web APIの呼び出し部分などは、テストダブルを使ってテスト可能にしておいた方が、外部の状況に依存しないテストを記述できます。

現在時刻の設計判断に関しては、詳しくはコード内で「現時刻」を気軽に取得してはいけない – nekoya pressを読んでみてください。

このアプローチのデメリット

このアプローチのデメリットは、場合によっては「やりすぎ」を招きやすいことです。

他のアプローチに比べてクラス数、ファイル数が増えていることからもわかるように、このアプローチは責務の分離/再配置と設計変更によって問題を解決しています。

テストのための設計が責務の再配置を促し、全体として責務がバランスしてシンプルになるのであれば、それはテストが良い設計へ導いてくれたということです。もしもテストのための設計が複雑さを招くなら、それは「やりすぎ」でしょう。

[ポイント] テストを設計ツールとして使う

テストを書くと、必要十分な設計に気づきやすくなります。

設計は終わりのない世界です。そして、設計について考えすぎると、終わりがない世界に踏み込んでいることに気づきにくくなります。このようなとき、テストの結果/ゴール/具体例から考えることで「あれもできる、これもできるかも、ああいうやりかたもあるな」というフワフワとした状態から、ピントのあった状態に戻ることができます。

テストコードと共に設計/実装を行うと、コードを書くことや、書いたコードをすぐ使うことから、設計に対するフィードバックが発生します。実際にテストとコードを書くことによって、設計だけしていたときは考え過ぎていて、もっとシンプルで良いことがわかったり、または逆に設計時だけでは考えが足りておらず、実際にコードを書いたり具体的な値でテストするようになって考慮不足がわかる、という状況によく出会います。

テストを書くことは、終わりのない設計の世界から現実に戻ってくるきっかけのひとつになります。良い設計は状況によって変わります。必要十分でシンプルな設計から、次のシンプルな設計へと不安なく移るために、テストを活用してください

まとめ1: 参加言語とアプローチの内訳

ここまでで、今回のすべてのアプローチを見てきました。今回のお題では、テスト容易性のためのアプローチは、大きく4つに分かれました。

  • 現在時刻を引数で渡す
  • 時刻ライブラリに介入
  • テスト対象の部分オーバーライド
  • 時刻アクセス用コラボレータ導入

参加者の皆様のアプローチを、以下の表にまとめてみました。

名前 言語 テスティングフレームワーク アプローチ アプローチ詳細
ciel Ruby 2.0 RSpec 2.14 現在時刻を引数で渡す greet メソッドへ日付を引数渡し
こねこねこ PHP 5.5 テスト用 main 現在時刻を引数で渡す greet メソッドへ日付を引数渡し
ganchiku PHP 5.4 phpspec 2.0 現在時刻を引数で渡す greet への引数渡し & 渡す日付をモック
tbpgr Ruby 1.9.3 RSpec 2.14 時刻ライブラリに介入 標準ライブラリ戻り値を Timecop で固定
ynak Ruby 1.9.3 Rspec 2.14 時刻ライブラリに介入 標準ライブラリを RSpec で stub
ishiduca JavaScript QUnit & qunit-tap 時刻ライブラリに介入 標準ライブラリを自前 stub
TatsushiD Perl Test::More 時刻ライブラリに介入 標準関数をテスト内で上書き
きんきん C#4.0 Visual Studio 2010 時刻ライブラリに介入 標準ライブラリを Moles で stub
antimon2 Ruby 2.0 test::unit 時刻ライブラリに介入 標準ライブラリを自前サブクラスでstub
kencharos Java7 Junit 4.4 テスト対象の部分オーバーライド 自作メソッド current をテスト内サブクラスでオーバーライド
anagotan Java7 JUnit 4 テスト対象の部分オーバーライド テスト対象クラスを Self Shunt
ushiboy JavaScript Jasmine テスト対象の部分オーバーライド テスト対象のメソッド getNow を stub/spy
tdoi Java JUnit 4 + JMock 2.6 時刻アクセス用コラボレータ導入 Environment オブジェクトを作成してモック & セッターインジェクション
Touchu Java7 JUnit 4.11 + Mockito 1.9.5 時刻アクセス用コラボレータ導入 現在時刻の Factory をモック & コンストラクタインジェクション

これらのアプローチには、どれかが絶対の正解、というものはありません。本稿では、これらのアプローチには、すべてメリットとデメリットがあることを説明してきました。プログラミング言語の動的/静的の性格やテスティングフレームワーク、モックライブラリの能力によってテスト容易性設計も異なります。大事なのは、状況に合わせてシンプルで適切なアプローチを選択することです。

まとめ2: テストコードのポイント

最後に、各改善点の部分などで都度説明してきた「ポイント」をまとめておきます。

良いユニットテストは Repeatable (繰り返し可能、再現可能)

  • テストダブルを使いこなす
  • 外部環境との界面にインターフェイスを作成し、テストダブルで置き換える

良いユニットテストは独立 (Independent) していなければならない

  • 後始末を忘れずに行い、テストを独立させる
  • static を避け、テストメソッド間の依存関係を断つ

Assertion Roulette に注意する

  • 目指すのは「テストメソッド毎にアサーションひとつ」(しかし、やりすぎは禁物)
  • カスタムアサーションを使う
  • パラメタライズドテスト(Parameterized Test)を使いこなす

Fragile Test (脆いテスト) に注意する

  • テストだけに使う部分の可視性を下げる
  • private メソッドを扱いたくなったら要注意

テストを設計ツールとして使う

  • テストコードのノイズを減らす
  • 日本語テストメソッドを試してみる
  • シンプルなコードとテスト失敗時の情報のバランスを考える

これらのポイントを考えながらテストコードを書くことで、テスト容易性を考慮した設計が見えてくるはずです。本稿を参考にして、読者の皆様もぜひ自分のコードのテスト容易性設計を考えてみてください。

◆CodeIQで出題中!テスト駆動開発に興味もったら挑戦してみよう!◆

寄稿者プロフィール 和田 卓人
タワーズ・クエスト株式会社プログラマ兼取締役社長。テスト駆動開発に関する文章や講演、ハンズオンイベントなどを通じてテスト駆動開発を広めようと努力している。今日もグリーンバンド(テスト駆動開発者の証)を左手に着け、テストと共にコードを書いている。『プログラマが知るべき97のこと』『SQLアンチパターン』(オライリージャパン)監修。

twitter: @t_wada
github: https://github.com/twada
mail: takuto.wada アット gmail.com

CodeIQコード銀行にあなたのコードを預けてみませんか?

  • CodeIQコード銀行ではあなたのコードを財産と考えます。
  • お預かりいただいたコードは、CodeIQコード銀行がしっかり評価し、フィードバックいたします。
  • 当コード銀行にお預けいただいたコードは、企業がみてスカウトをかける可能性があります。
  • 転職したい方や将来転職することを考えている方で、今の自分のスキルレベルを知りたい方はぜひ挑戦してみてください。
  • 企業からスカウトがきたら困る人は挑戦しないでください。

興味を持った方はこちらからチャレンジを!

  • 211
  • このエントリーをはてなブックマークに追加

■関連記事

和田卓人さん出題のテスト駆動開発問題『現在時刻とロケールに依存するテスト』をPHPを使ってオブジェク... PHPメンターズの後藤です。 和田卓人さん出題の『現在時刻とロケールに依存するテスト』問題をPHPを使ってオブジェクト指向のアプローチで解答してみました。 ※問題文については、和田卓人さんの解説記事を参照にしてください。 https://codeiq.jp/magazine/2013/11/14...

今週のPickUPレポート

新着記事

週間ランキング

CodeIQとは

CodeIQ(コードアイキュー)とは、自分の実力を知りたいITエンジニア向けの、実務スキル評価サービスです。

CodeIQご利用にあたって
関連サイト
codeiq

リクルートグループサイトへ