メモ: JUnitのexpectedフィールドとRuleアノテーションで例外のテスト

次の記事を読んで、初めて知ったのでメモ。

このようなテスト対象のコードがあるとします。

import java.util.ArrayList;
import java.util.List;

public class Deck {
        /**
         * 指定した人数のプレイヤーに、山札を均一に配る。均一に配れない場合、プレイヤー間の配布枚数の差異は1枚以内とする。
         */
        public List<List<Card>> divideCards(int playerNum) {

                if (playerNum < 2) {
                        throw new IllegalArgumentException("プレイヤー数は2以上でなければならない。");
                }

                List<List<Card>> result = new ArrayList<List<Card>>(playerNum);
                // 略
                return result;
        }
}

class Card {
        // 略
}

divideCardsメソッドは、山札をプレイヤーに配ります。ここで、ゲーム中のプレイヤーが、2人以上であることを想定しています。もし1人以下のプレイヤーに対して山札を配ろうとすると、例外IllegalArgumentExceptionが発生します。

このコードのテストは、これまでこんな感じで書いてきました。

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.junit.Test;

public class DeckTest {
        Deck deck;

        @Test
        public void testDivideCardsForException() throws Exception {
                deck = new Deck();

                try {
                        deck.divideCards(0);
                        fail("ここには来ない");
                } catch (IllegalArgumentException expected) {
                        assertEquals("例外のメッセージ確認", expected.getMessage(),
                                        "プレイヤー数は2以上でなければならない。");
                }
        }
}

しかし、今はこんなふうに簡潔に書けるのですね。

import org.junit.Test;

public class DeckTest {
        Deck deck;

        @Test(expected = IllegalArgumentException.class)
        public void testDivideCardsForException() throws Exception {
                deck = new Deck();
                deck.divideCards(0);
        }
}

Testアノテーションのexpectedフィールドに、実行時に発生する例外クラスを指定するのがポイントです。

しかし、この書き方には欠点もあるようです。

たとえば、さっきと少し違って、テスト対象のコードがこんな感じだとします。

import java.util.ArrayList;
import java.util.List;

public class Deck {
        /**
         * 指定した人数のプレイヤーに、山札を均一に配る。均一に配れない場合、プレイヤー間の配布枚数の差異は1枚以内とする。
         */
        public List<List<Card>> divideCards(int cardNum, int playerNum) {

                if (cardNum < 1) {
                        throw new IllegalArgumentException("山札は1枚以上なければならない。");
                }
                if (playerNum < 2) {
                        throw new IllegalArgumentException("プレイヤー数は2以上でなければならない。");
                }

                List<List<Card>> result = new ArrayList<List<Card>>(playerNum);
                // 略
                return result;
        }
}

class Card {
        // 略
}

上のコードでは、divideCardsメソッドの引数が2つになっていて、IllegalArgumentExceptionを投げる原因が、「1つ目の引数が不正なとき」と「2つ目の引数が不正なとき」の2パターンに増えています。(もちろん、テストケースの考え方によっては、「両方誤っているとき」も加わりますが、それはさておき)

expectedフィールドを使う書き方では、テスト対象コードに例外を投げる原因が複数あるとき、それぞれの原因を識別できません。IllegalArgumentExceptionが飛んできたことだけしか検出できないのですね。

そういうときは、こんなふうに書けばよいそうです。

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class DeckTest {

	Deck deck;

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	@Test
	public void testDivideCardsForDeckException() throws Exception {
		deck = new Deck();

		thrown.expect(IllegalArgumentException.class);
		thrown.expectMessage("山札は1枚以上なければならない。");
		deck.divideCards(0, 2);
	}

	@Test
	public void testDivideCardsForPlayerException() throws Exception {
		deck = new Deck();

		thrown.expect(IllegalArgumentException.class);
		thrown.expectMessage("プレイヤー数は2以上でなければならない。");
		deck.divideCards(2, 0);
	}
}

Ruleアノテーションをつけた、ExpectedException型のフィールド変数がポイントです。この変数に例外の型やメッセージを設定して、テストを実行すると、検証を行えます。

メモは以上です。

(2012/10/3 追記)1つのメソッドに、テスト対象コード呼び出しを2回書いていましたが、分けました。1回目の呼び出ししか実行されないため、間違っていました。shuji_w6eさん、backpaper0さん、ご指摘ありがとうございます。