JUnitのテストルールの作り方

投稿日:  更新日:

新たなテストルールの作り方を紹介します。

JUnitとAndroidJUnitにはいくつかの有用なテストルールがすでに用意されています。しかし、十分とは言えません。テストルールを自作してテスト環境を機能拡張しましょう!

スポンサーリンク

テストルールの役割

テストルールはテストに必要なボイラープレートコードを削減します。

ボイラープレートとは「省く事ができない定型的な処理」のことです。テストを記述すると、各テストにボイラープレートコードが頻繁に出てきます。

例えばActivityがありA、B、Cの3つの機能を持つとします。この3つの機能をテストする時、各々のテストには準備でActivityを起動し、終わりでActivityを停止する処理が必要になります。Activityの機能をテストするのですから、Activityの起動と停止が必要なのは当たり前ですよね。

テストクラスにActivityの起動と停止を記述すると次のようになります。この記述がボイラープレートコードです。

KotlinJava
@RunWith(AndroidJUnit4::class)
class SampleATest {

    lateinit var activity: Activity
    @Before
    fun setUp() {
        val instrumentation = InstrumentationRegistry.getInstrumentation()
        val context = instrumentation.targetContext
        val intent = Intent(context, MainActivity::class.java)
        activity = instrumentation.startActivitySync(intent)
        instrumentation.waitForIdleSync()
    }
    @After
    fun tearDown() {
        activity.finish()
    }

    @Test
    fun 機能Aのユースケース() {
        // 機能Aのユースケースをチェック
    }
}
@RunWith(AndroidJUnit4.class)
public class SampleATest {

    private Activity mActivity;
    @Before
    public void setUp() {
        Instrumentation _Instrumentation
                = InstrumentationRegistry.getInstrumentation();
        Context _Context = _Instrumentation.getTargetContext();
        Intent _Intent = new Intent(_Context, MainActivity.class);
        mActivity = _Instrumentation.startActivitySync(_Intent);
        _Instrumentation.waitForIdleSync();
    }
    @After
    public void tearDown() {
        mActivity.finish();
    }

    @Test
    public void 機能Aのユースケース() {
        // 機能Aのユースケースをチェック
    }
}

AndroidJunitにルールActivityTestRuleがあります。これは例に挙げたActivityの起動と停止を行うテストルールです。ルールActivityTestRuleを使うと次のようになります。

KotlinJava
@RunWith(AndroidJUnit4::class)
class SampleA_Test {

    @get:Rule
    val activityTestRule = ActivityTestRule(MainActivity::class.java)
    @Before
    fun setUp() {
    }
    @After
    fun tearDown() {
    }

    @Test
    fun 機能Aのユースケース() {
        // 機能Aのユースケースをチェック
    }
}
@RunWith(AndroidJUnit4.class)
public class SampleA_Test {

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule
            = new ActivityTestRule<>(MainActivity.class);
    @Before
    public void setUp() {
    }
    @After
    public void tearDown() {
    }

    @Test
    public void 機能Aのユースケース() {
        // 機能Aのユースケースをチェック
    }
}

ボイラープレートコードがなくなり、シンプルになりました。

※実際のルールActivityTestRuleは内部でもっと複雑な処理をしています。

スポンサーリンク

テストルールの作成

テストクラスにボイラープレートコードが出てきたら、新たなテストルールを作ってみましょう。テストルールに置き換えると記述もすっきりするし、作ったテストルールは後々のテストで再利用できるし、一石二鳥ですよ。

作成方法は簡単です。TestRuleを継承したクラスを作り、テストの実行(Statement#evaluate)を前処理(before)、後処理(after)で挟むだけです。

KotlinJava
class CustomRule : TestRule {

    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            override fun evaluate() {
                try {
                    before()        // 前処理
                    base.evaluate() // テストの実施
                } finally {
                    after()         // 後処理
                }
            }
        }
    }

    private fun before() {
        ここで前処理を行う
    }
    private fun after() {
        ここで後処理を行う
    }
}
public class CustomRule implements TestRule {

    @Override
    public Statement apply(final Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                try {
                    before();           // 前処理
                    base.evaluate();    // テストの実施
                }
                finally {
                    after();            // 後処理
                }
            }
        };
    }

    private void before() {
        ここで前処理を行う
    }
    private void after() {
        ここで後処理を行う
    }
}

注意点はtry{…}finally{…}を使い、後処理はfinally{…}に記述することです。これはテストがfailになったとき、後処理が必ず実行されることを保証するためです。

スポンサーリンク

テストルールの処理の順番は?

複数のテストルールをテストクラスに記述したとき、どういう順番で実行されるのか、気になったので調べてみました。

KotlinJava
@RunWith(AndroidJUnit4::class)
class CustomRule_Test {
    @get:Rule
    val ruleA = CustomRule_A() // (3)
    @get:Rule
    val ruleB = CustomRule_B() // (2)
    @get:Rule
    val ruleC = CustomRule_C() // (1)
    @Before
    fun setUp() {
        Log.i("CustomRule_Test", "Execute setUp")
    }
    @After
    fun tearDown() {
        Log.i("CustomRule_Test", "Execute tearDown")
    }

    @Test
    fun カスタムルールの順番をテスト() {
        Log.i("CustomRule_Test", "テストの実施")
    }
}
@RunWith(AndroidJUnit4.class)
public class CustomRule_Test {

    @Rule
    public CustomRule_A mRuleA = new CustomRule_A();    // (3)
    @Rule
    public CustomRule_B mRuleB = new CustomRule_B();    // (2)
    @Rule
    public CustomRule_C mRuleC = new CustomRule_C();    // (1)
    @Before
    public void setUp() {
        Log.i("CustomRule_Test", "Execute setUp");
    }
    @After
    public void tearDown() {
        Log.i("CustomRule_Test", "Execute tearDown");
    }

    @Test
    public void カスタムルールの順番をテスト() {
        Log.i("CustomRule_Test", "テストの実施");
    }
}
I/CustomRule_C: Execute before
I/CustomRule_B: Execute before
I/CustomRule_A: Execute before
I/CustomRule_Test: Execute setUp
I/CustomRule_Test: テストの実施
I/CustomRule_Test: Execute tearDown
I/CustomRule_A: Execute after
I/CustomRule_B: Execute after
I/CustomRule_C: Execute after

順番はC⇒B⇒A⇒テスト⇒A⇒B⇒Cとなります。

ただ、実行順に依存関係のあるルールは、原因が解明しづらいfailを生むことになりそうなので避けるべきだと思います。

スポンサーリンク
スポンサーリンク