JUnitの@FixMethodOrderで優先度の高いものからテスト

投稿日:  更新日:

テストを実行するとき、「優先度の高いものからテストを実行して欲しい」と思いませんか!

なぜなら「Bを行うためにはAになっている必要がある」とか、アプリの動作には手順があるからです。必然的にA>Bの優先順位が付きます。

優先度を無視してBの後にAをテストしたら、Aの意味がありません。

ですが…

テストプログラム中でテストを順番に並べたとしても、バラバラな順番で実行されてしまいます。

この問題を解決して、優先度の高いものからテストを実行する方法を紹介します。

スポンサーリンク

JUnitがテストを実行する順番

テストを実行する順番はどのように決まるのでしょうか?!

テストはRunnerというプログラムが実行していきます。なので、順番を決めているのはRunnerです。

例えば次のようなテストクラスがあるとします。

KotlinJava
@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がDEFAULTの場合のテスト結果

スポンサーリンク

FixMethodOrderで実行順を制御

今まで述べてきたテストの実行順ですが、@FixMethodOrderアノテーション使うと制御できます。タイプは3つです。

MethodSorters.XXX実行順序
DEFAULT
(FixMethodOrderが無い場合)
メソッド名のハッシュ値の昇順
順番は一意に定まる
JVMリフレクションのClass#getDeclaredMethods()で取得した順
順番は実行毎に変化する可能性あり
NAME_ASCENDINGメソッド名の辞書順(アルファベット順)
順番は一意に定まる

例えばテスト名(メソッド名)の辞書順にしたければ次のようにします。

KotlinJava
@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のNAME_ASCENDING(辞書順)を使います。

まず、検証項目に優先度を定義します。優先度はアルファベットと数値で表します。若い文字の方が優先度が高いです。

優先度を定義したテスト項目の例
※上記は例です。カテゴリ分けや優先度の付け方は色々な工夫ができると思います。

次に、検証項目に定義した優先度を、テスト名(メソッド名)の頭へ追加した記述に修正します。

KotlinJava
@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を実行() { }
}

これにより、アルファベットと数値でソートされて、優先度順に実行されます。

優先度順のテスト結果

スポンサーリンク

関連記事:

新たなテストルールの作り方を紹介します。 JUnitとAndroidJUnitにはいくつかの有用なテストルールがすでに用意されています。しかし、十分とは言えません。テストルールを自作してテスト環境を機能拡張しましょう! ...
多言語対応したアプリのテストで、対応地域の動作を確認するためにロケールが変更できたら便利です。なので、ロケールを変更するテストルールを作成してみました。 ...
GUIを用いたアプリのテストで、テストの開始前にアニメーションを無効化することが定石となっています。理由は「テストの安定性を確保するためである」と、ドキュメントに記載されています。 この無効化の処理を自動で行えたら便利です。なので、アニメーションを無効にするテストルールを作成してみました。 ...
スケジュール管理やアラーム機能を実装するアプリのテストで、端末の時刻を自由に変更できたら便利です。なので、端末の時刻を変更するテストルールを作成してみました。 この記事は「Android端末の時刻を変更するJUnitテストルール(Api23~34対応)」で改訂されました。 ...
スケジュール管理やアラーム機能を実装するアプリのテストで、端末の日付を自由に変更できたら便利です。なので、端末の日付を変更するテストルールを作成してみました。 ※この記事は「Android端末の日付を変更するJUnitテストルール(Api23~34対応)」で改訂されました。 ...
地図を表示して自身の地理的位置(Geolocation)を管理するアプリのテストで、疑似的に端末の位置が変更できたら便利です。なので、端末の位置を変更するテストルールを作成してみました。 ...
スケジュール管理やアラーム機能を提供するアプリのテストで、端末の日付を自由に変更できたら便利です。 ですので、端末の日付を変更するテストルールを作成してみました。 この記事は、以前に投稿した「Android端末の日付を変更するJUnitテストルール」を改訂したものです。 API23~34で動作するように、テストルールの記述を改良しています。 ※環境:Android Studio Jellyfish | 2023.3.1     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.test.uiautomator:uiautomator 2.3.0     androidx.test.ext:junit 1.1.5     androidx.test:rules 1.5.0     junit:junit 4.13.2 ...
スケジュール管理やアラーム機能を提供するアプリのテストで、端末の時刻を自由に変更できたら便利です。 ですので、端末の時刻を変更するテストルールを作成してみました。 この記事は、以前に投稿した「Android端末の時刻を変更するJUnitテストルール」を改訂したものです。 API23~34で動作するように、テストルールの記述を改良しています。 ※環境:Android Studio Jellyfish | 2023.3.1     Kotlin 1.9.0     Compose Compiler 1.5.1     androidx.test.uiautomator:uiautomator 2.3.0     androidx.test.ext:junit 1.1.5     androidx.test:rules 1.5.0     junit:junit 4.13.2 ...
スポンサーリンク