2009年12月25日金曜日

MapReduceをJUnitでTDDで作る(8 テストの追加)

Merry, Xmas!
最近のアメリカではHappy Holidays!!と言うのが一般的だそうです。
12/25で年始まで休日か・・うらやましい(ぉぃ)

閑話休題。前回で、リファクタリングを行い、
valueに与えられた文字列をそのままmapperがwriteするようにしました。
しかし、これでは仕様と異なることが明白です。

常にINFOを含む行が来るという仮定ならば、これでよいのですが、そうとは限りません。
では、実際にテストしてREDとなることを確認しましょう。

INFOを含む行だけでなく、WARNINGを含む行やERRORを含む行が来ることが予想されるので、追加するテストは、次のようなテストデータを用いればいいと予想できます。

        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu",
                "2009-12-14 00:00:26,341 WARN  fugafuga @ 11111111111111111111111",
                "2009-12-14 00:00:26,342 ERROR fatal @ exception has occurred",
        };
また、INFOを含む行のみが抽出されるはずなので、context.writeの呼び出しは次のようになるはずです。
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));

さらに、writeの呼び出しは一度しか行われない筈なので、次のverifyを追加しましょう。

        verify(context, new Times(1)).write(any(Text.class), any(LongWritable.class));

テストメソッド名は  testMapExtract_ComplexRows()にしてみました。しかし、anyがimportされてないので、コンパイルが通りません。次のstatic import文を追加して下さい。

import static org.mockito.Matchers.any;
これでコンパイルまで通った筈です。

テストメソッドは次のようになります。
    /**
     * 抽出する行が複数の場合
     * @throws Exception 例外発生時
     */
    @Test
    public void testMapExtract_ComplexRows() throws Exception {
        Mapper.Context context = mock(Mapper.Context.class);
        String[] lines = {
                "2009-12-14 00:00:26,340 INFO  hogehoge @ abcdefg hijklmn opqrstu",
                "2009-12-14 00:00:26,341 WARN  fugafuga @ 11111111111111111111111",
                "2009-12-14 00:00:26,341 ERROR fatal @ exception has occurred",
        };
        LogAnalysisMapper mapper = new LogAnalysisMapper();
        mapper.map(null, new Text(line[0]), context);
        verify(context, new Times(1)).write(new Text(lines[0]), new LongWritable(1));
        verify(context, new Times(1)).write(any(Text.class), any(LongWritable.class));
    }


では、実行してみましょう。REDになるはずなのですが・・・。

 GREENになってしまいました。

これはおかしいですね。REDになるはずなのにGREENになってしまったということは、テストがどこかおかしいと言うことです。(GREENの理由を明確に説明できるのならばいいのですが、今回はREDを予想していたので明らかにおかしい)

GREENになってしまったということは、context.write();が一度しか呼ばれていないということです。
今のmap()メソッド内では無条件にcontext.write()を呼んでいるので、つまりmapメソッドが一度しか呼ばれていないと言うことですね。
つまり、map()メソッドを引数を変えて複数回呼べばREDになるはずですね。

ということで以下のように変更します。
        mapper.map(null, new Text(lines[0]), context);
         for (String line : lines) {
            mapper.map(null, new Text(line), context);
        }
せっかく複数行のテストデータを用意したのに最初の1行しかテストに用いてなかったようです。
全行をテストで呼ぶように変更しました。

では気を取り直して、テストを再実行してみます。


ようやく想定通り、REDになりました。
(一つ目はGREENのままです。これも想定通りです)
失敗内容を確認してみましょう。
org.mockito.exceptions.verification.TooManyActualInvocations:
context.write(
    isA(org.apache.hadoop.io.Text),
    isA(org.apache.hadoop.io.LongWritable)
);
Wanted 1 time but was 3
    at jp.co.littel.hadoop.TestLogAnalysisMapper.testMapExtract_ComplexRows(TestLogAnalysisMapper.java:72)
Caused by: org.mockito.exceptions.cause.UndesiredInvocation:
(以下略)
呼び出しが多すぎるというエラーになっています。
「想定は1回なのに3回呼ばれた」と言っています。これは想定通りの結果です。
(テストデータは3行なのでmap()メソッドは3回呼ばれる。map()メソッド内部では無条件にcontext.write()メソッドを呼び出すので3回呼ばれてしまう)

では、ソースを修正して、REDをGREENにします。
INFOの場合のみ取り出せばいいので、
        if (value.toString().contains("INFO")) {
            context.write(value, new LongWritable(1));
        }

でよさそうです。
(優秀な開発者の方はこの時点でバグを見つけられると思います。またテストを追加してそれを明らかにしていこうと思います。)

では、実行してみます。
おめでとうございます。すべてGREENになりました!



GREENになったらリファクタリングです。
リファクタリングは次回に。。。

0 件のコメント:

コメントを投稿