テストを実行するとき、「優先度の高いものからテストを実行して欲しい」と思いませんか!
なぜなら「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を実行() { } }
これにより、アルファベットと数値でソートされて、優先度順に実行されます。
関連記事: