テストを実行するとき、「優先度の高いものからテストを実行して欲しい」と思いませんか!
なぜなら「Bを行うためにはAになっている必要がある」とか、アプリの動作には手順があるからです。必然的にA>Bの優先順位が付きます。
優先度を無視してBの後にAをテストしたら、Aの意味がありません。
ですが…
テストプログラム中でテストを順番に並べたとしても、バラバラな順番で実行されてしまいます。
この問題を解決して、優先度の高いものからテストを実行する方法を紹介します。
JUnitがテストを実行する順番
テストを実行する順番はどのように決まるのでしょうか?!
テストはRunnerというプログラムが実行していきます。なので、順番を決めているのはRunnerです。
例えば次のようなテストクラスがあるとします。
@RunWith(AndroidJUnit4::class)
class DefaltOrderTest {
companion object {
@BeforeClass @JvmStatic
fun setUpC() {
val _Methods = DefaltOrderTest::class.java.declaredMethods
for (i in _Methods.indices) {
Log.i("setUp", "Hash = %11d Methods = %s".format(
_Methods[i].name.hashCode(),
_Methods[i].name
))
}
}
}
@Before fun setUp() { }
@After fun tearDown() { }
@Test fun 画面表示_メニューバー() { }
@Test fun Fileメニュー_プルダウンを開く() { }
@Test fun Fileメニュー_プルダウンを閉じる() { }
@Test fun Fileメニュー_Newを実行() { }
@Test fun Fileメニュー_Openを実行() { }
@Test fun Editメニュー_プルダウンを開く() { }
@Test fun Editメニュー_プルダウンを閉じる() { }
@Test fun Editメニュー_Undoを実行() { }
@Test fun Editメニュー_Redoを実行() { }
}
@RunWith(AndroidJUnit4.class)
public class DefaultOrderTest {
@BeforeClass
public static void setUpC() {
Method[] _Methods = DefaultOrderTest_j.class.getDeclaredMethods();
for(int i = 0; i < _Methods.length; i++) {
Log.i("setUp", String.format("Hash = %11d Methods = %s",
_Methods[i].getName().hashCode(),
_Methods[i].getName()
));
}
}
@Before public void setUp() { }
@After public void tearDown() { }
@Test public void 画面表示_メニューバー() { }
@Test public void Fileメニュー_プルダウンを開く() { }
@Test public void Fileメニュー_プルダウンを閉じる() { }
@Test public void Fileメニュー_Newを実行() { }
@Test public void Fileメニュー_Openを実行() { }
@Test public void Editメニュー_プルダウンを開く() { }
@Test public void Editメニュー_プルダウンを閉じる() { }
@Test public void Editメニュー_Undoを実行() { }
@Test public void Editメニュー_Redoを実行() { }
}
Runnerはテストを実行する前にリフレクションのClass#getDeclaredMethods()を使ってテストメソッドの一覧を取得します。
このメソッドの一覧を次のようなComparatorを使ってソートしています。その結果がテストを実行する順番になるのです。
public class MethodSorter {
/**
* DEFAULT sort order
*/
public static final Comparator<Method> DEFAULT = new Comparator<Method>() {
public int compare(Method m1, Method m2) {
int i1 = m1.getName().hashCode();
int i2 = m2.getName().hashCode();
if (i1 != i2) {
return i1 < i2 ? -1 : 1;
}
return NAME_ASCENDING.compare(m1, m2);
}
};
...
public static Method[] getDeclaredMethods(Class<?> clazz) {
Comparator<Method> comparator = getSorter(clazz.getAnnotation(FixMethodOrder.class));
Method[] methods = clazz.getDeclaredMethods();
if (comparator != null) {
Arrays.sort(methods, comparator);
}
return methods;
}
...
}
デフォルトのCamparatorはメソッド名のハッシュ値で並び変えます。なので、例にあげたテストクラスは次の順番になります。
【ソート前(@BeforCalssで出力)】
I/setUp: Hash= -905798330 Methods= setUpC I/setUp: Hash= -426626835 Methods= Editメニュー_Redoを実行 (4) I/setUp: Hash= -1801420729 Methods= Editメニュー_Undoを実行 (1) I/setUp: Hash= 1706913455 Methods= Editメニュー_プルダウンを閉じる (9) I/setUp: Hash= 609250705 Methods= Editメニュー_プルダウンを開く (7) I/setUp: Hash= 896947137 Methods= Fileメニュー_Newを実行 (8) I/setUp: Hash= -1451663981 Methods= Fileメニュー_Openを実行 (2) I/setUp: Hash= 511379105 Methods= Fileメニュー_プルダウンを閉じる (6) I/setUp: Hash= -1368977569 Methods= Fileメニュー_プルダウンを開く (3) I/setUp: Hash= 109328029 Methods= setUp I/setUp: Hash= -1664427484 Methods= tearDown I/setUp: Hash= 181016979 Methods= 画面表示_メニューバー (5) ※末尾の( )はハッシュ値の小さい順
【ソート後(テストの結果)】

FixMethodOrderで実行順を制御
今まで述べてきたテストの実行順ですが、@FixMethodOrderアノテーション使うと制御できます。タイプは3つです。
| MethodSorters.XXX | 実行順序 |
|---|---|
| DEFAULT (FixMethodOrderが無い場合) | メソッド名のハッシュ値の昇順 順番は一意に定まる |
| JVM | リフレクションのClass#getDeclaredMethods()で取得した順 順番は実行毎に変化する可能性あり |
| NAME_ASCENDING | メソッド名の辞書順(アルファベット順) 順番は一意に定まる |
例えばテスト名(メソッド名)の辞書順にしたければ次のようにします。
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(AndroidJUnit4::class)
class DefaltOrderTest {
@Test fun 画面表示_メニューバー() { }
@Test fun Fileメニュー_プルダウンを開く() { }
@Test fun Fileメニュー_プルダウンを閉じる() { }
@Test fun Fileメニュー_Newを実行() { }
@Test fun Fileメニュー_Openを実行() { }
@Test fun Editメニュー_プルダウンを開く() { }
@Test fun Editメニュー_プルダウンを閉じる() { }
@Test fun Editメニュー_Undoを実行() { }
@Test fun Editメニュー_Redoを実行() { }
}
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(AndroidJUnit4.class)
public class DefaultOrderTest_j {
@Test public void 画面表示_メニューバー() { }
@Test public void Fileメニュー_プルダウンを開く() { }
@Test public void Fileメニュー_プルダウンを閉じる() { }
@Test public void Fileメニュー_Newを実行() { }
@Test public void Fileメニュー_Openを実行() { }
@Test public void Editメニュー_プルダウンを開く() { }
@Test public void Editメニュー_プルダウンを閉じる() { }
@Test public void Editメニュー_Undoを実行() { }
@Test public void Editメニュー_Redoを実行() { }
}

日本語を含んでいても、アルファベット、ひらがな、カタカナ、漢字の順に並ぶようです。
優先度の高いものからテスト
優先度の高いものからテストするために、@FixMethodOrderのNAME_ASCENDING(辞書順)を使います。
まず、検証項目に優先度を定義します。優先度はアルファベットと数値で表します。若い文字の方が優先度が高いです。

※上記は例です。カテゴリ分けや優先度の付け方は色々な工夫ができると思います。
次に、検証項目に定義した優先度を、テスト名(メソッド名)の頭へ追加した記述に修正します。
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(AndroidJUnit4::class)
class NameOrderTest {
@Test fun A1_画面表示_メニューバー() { }
@Test fun A2_Fileメニュー_プルダウンを開く() { }
@Test fun C1_Fileメニュー_プルダウンを閉じる() { }
@Test fun B1_Fileメニュー_Newを実行() { }
@Test fun B2_Fileメニュー_Openを実行() { }
@Test fun A2_Editメニュー_プルダウンを開く() { }
@Test fun C1_Editメニュー_プルダウンを閉じる() { }
@Test fun B3_Editメニュー_Undoを実行() { }
@Test fun B4_Editメニュー_Redoを実行() { }
}
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(AndroidJUnit4.class)
public class NameOrderTest {
@Test public void A1_画面表示_メニューバー() { }
@Test public void A2_Fileメニュー_プルダウンを開く() { }
@Test public void C1_Fileメニュー_プルダウンを閉じる() { }
@Test public void B1_Fileメニュー_Newを実行() { }
@Test public void B2_Fileメニュー_Openを実行() { }
@Test public void A2_Editメニュー_プルダウンを開く() { }
@Test public void C1_Editメニュー_プルダウンを閉じる() { }
@Test public void B3_Editメニュー_Undoを実行() { }
@Test public void B4_Editメニュー_Redoを実行() { }
}
これにより、アルファベットと数値でソートされて、優先度順に実行されます。

関連記事:
