レガシーコードも年末に向けて大掃除! - jMockitを使おう

さて、今年もあと僅かになりましたが、この1年間で溜まりに溜まったコードの負債を大掃除してみませんか?

レガシーコードだから書くのが大変? プロダクト側を改修したいけどそもそもテストが無いから改修が怖い? ですよねー。 なのでjMockitを使って既存のコードにテストを書く方法を紹介します。

まず、想定するレガシーコードですが検証したいメソッドの内部で現在時刻を取得してるケースを考えます。 普通にクラスやメソッドから日付をインジェクションできるように作るのが正しいコードですが、結構ありがちなのがこんなコード。

    public class MyCalendar {
        public Date nextDay() {
            Calendar calendar = SystemDate.getDate();
            calendar.add(Calendar.DATE, 1);
            return calendar.getTime();
        }
    }

SystemDateは検証環境なんかで結合テストしやすいように、設定ファイルやDBの値を読み込むことが多いでしょうけど、今回はシンプルに現在時刻だけ。

    public class SystemDate {
        public static Calendar getDate() {
            return Calendar.getInstance();
        }
    }

このようなケースでMyCalendar#nextDayにテストを書こうとしても日付が変更出来ないので、月またぎや年またぎ、うるう年のテストが出来ません。検証環境にデプロイして何らかの方法でシステム日付を変更し、手でポチポチと検証しながら画面のスクリーンショットを取る簡単なお仕事が待っていそうですね。

ユニットテストをする気がちっとも無い素敵な感じですが、残念なことにしばしば見かけるタイプのコードです。

jMockitを使うことで、こんな残念な設計に対しても、比較的低コストでUTを書くことが可能です。

    public class MyCalendarTest {

        String $(Date date) {
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
            return df.format(date);
        }

        Calendar getCalendar(int y, int m, int d) {
            Calendar cal = Calendar.getInstance();
            cal.set(y, m - 1, d);
            return cal;
        }

        @Test
        public void test01() {
            new Expectations() {
                @Mocked
                SystemDate systemDate;

                {
                    SystemDate.getDate();
                    result = getCalendar(2013, 12, 13);
                    result = getCalendar(2013, 12, 31);
                    result = getCalendar(2013, 12, 13);
                }
            };

            MyCalendar myCalendar = new MyCalendar();
            assertThat($(myCalendar.nextDay()), is("2013-12-14"));
            assertThat($(myCalendar.nextDay()), is("2014-01-01"));
            assertThat($(myCalendar.nextDay()), is("2013-12-14"));
        }
    }

Expectationsブロックの中にMockとして戻り値を差し替えたい内容を記述します。 MyCalendar#nextDay()の中で利用される"SystemDate.getDate()"の戻り値は直下に記載したresultのものに差し替えられます。 今回はstaticメソッドを置き換えましたが、もちろん同様にインスタンスメソッドも置き換え可能です。

また、重要なのはこれがクラス全てではなく部分差し替えとなっていることです。 これにより、対象クラス全体の振る舞いを書き直す手間やリスク無く、モックを作成することが可能です。

ちなみにこのコードで

    MyCalendar myCalendar = new MyCalendar();
    assertThat($(myCalendar.nextDay()), is("2013-12-14"));
    assertThat($(myCalendar.nextDay()), is("2014-01-01"));
    assertThat($(myCalendar.nextDay()), is("2013-12-14"));
    assertThat($(myCalendar.nextDay()), is("2013-12-14"));

とすると

    mockit.internal.UnexpectedInvocation: Unexpected invocation of:
    SystemDate#getDate()

という例外が発生してテストが失敗します。これは、resultの数以上呼び出されるのは期待値と異なるためです。 差し替える値自体は固定で良いが複数回呼びたいケースというのもありますよね? その場合にはExpectaionsの代わりにNonStrictExpectationsを使うことで解決します。

       @Test
        public void test01() {
            new NonStrictExpectations() {
                @Mocked
                SystemDate systemDate;

                {
                    SystemDate.getDate();
                    result = getCalendar(2013, 12, 13);
                    result = getCalendar(2013, 12, 31);
                    result = getCalendar(2013, 12, 13);
                }
            };

            MyCalendar myCalendar = new MyCalendar();
            assertThat($(myCalendar.nextDay()), is("2013-12-14"));
            assertThat($(myCalendar.nextDay()), is("2014-01-01"));
            assertThat($(myCalendar.nextDay()), is("2013-12-14"));

            // 最後のresultとinstanceが同じなので12/14の更に翌日になる
            assertThat($(myCalendar.nextDay()), is("2013-12-15"));
        }

ね、簡単でしょ? これで、今まで初期化が大変過ぎて二の足を踏んでいたコード群にもガンガンテストを書いていくことが出来ます。 それではみなさんも良い年末を! Happy Hacking!

参考: