はじめに

前提としてモッキングライブラリはmockk、アサーションライブラリはGoogleのTruthを使うとします。他のライブラリを使っている場合は適宜読み替えてください。

例題

例として下記のようなクラスがあったとします。

internal class LoadDataUseCaseImpl(
        private val dao: DataAccessObject
) : LoadDataUseCase {
    override suspend fun execute(): List<Data> =
            dao.load()
}

// DataAccessObjectはRoomで書かれていて
// load関数自体もsuspend functionで書かれているとします
@Dao
interface DataAccessObject {
    @Query("select * from data")
    suspend fun load(): List<Data>
}

この場合、LoadDataUseCaseImplクラスのインスタンスを作成し、execute()を呼び出したらdao.load()が1回呼び出されるというユニットテストを書けば良さそうです。

suspend functionのテストを実施するには

Androidのユニットテストでsuspend functionのテストをする場合はrunBlockingTestを使用します。org.jetbrains.kotlinx:kotlinx-coroutines-test:VERSIONで入れられます。またdao.load()もLoadDataUseCaseImplインスタンスのexecute()どちらもsuspend functionで書かれているので呼び出す場合もrunBlockingTestが必要になります。

ユニットテストを書く

実際のテストコードは下記のようになります。

@RunWith(AndroidJUnit4::class)
class LoadDataUseCaseImplTest {
    @MockK
    private lateinit var dao: DataAccessObject // 実際に動作しなくて良いので@MockKアノテーションを付けてモックにします。
    private lateinit var useCase: LoadDataUseCaseImpl

    @Before
    fun setUp() {
        MockKAnnotations.init(this, relaxed = true, relaxUnitFun = true) // モックを注入します
        useCase = LoadDataUseCaseImpl(dao) // LoadDataUseCaseImplインスタンスを作成します。
    }

    @Test
    fun execute_callDaoLoadOnce() {
                // dao.load()が呼ばれたら何もしないようにします
        // coEvery { runBlockingTest { } }をすると戻り値を書かなくてもよくなります
                // suspend functionなのでrunBlockingTestが必要です
        coEvery { runBlockingTest { dao.load() } } just Runs

                // suspend functionなのでrunBlockingTestが必要です
        runBlockingTest { useCase.execute() }

                 // dao.load()が1回呼ばれることを確認する
                 // suspend functionなのでrunBlockingTestが必要です
        coVerify(exactly = 1) { runBlockingTest { dao.load() } }
        confirmVerified(dao)
    }

    @After
    fun tearDown() {
        unmockkAll()
    }
}

runBlockingTestが多すぎると思いますがsuspend functionを呼び出す度に必要になるのでこのようになります。