📱
Android Test Mockk 入門
Mockkはテストダブルを作るためのライブラリ
他に有名なものにmockitoがある。
今回はMockkがKotlinベースで作られていることCoroutinesをサポートしていることからMockkの使い方をメモしていく
基本
依存の追加
testImplementation("io.mockk:mockk:1.13.11")
テストの準備
同期的なクラスと非同期的なクラスを用意する
同期
interface ArithmeticCalculator {
fun add(a: Int, b: Int): Int
fun subtract(a: Int, b: Int): Int
fun multiply(a: Int, b: Int): Int
fun divide(a: Int, b: Int): Int
}
class Calculator(
private val arithmeticCalculator: ArithmeticCalculator
) {
fun add(a: Int, b: Int): Int {
return arithmeticCalculator.add(a, b)
}
fun subtract(a: Int, b: Int): Int {
return arithmeticCalculator.subtract(a, b)
}
fun multiply(a: Int, b: Int): Int {
return arithmeticCalculator.multiply(a, b)
}
fun divide(a: Int, b: Int): Int {
return arithmeticCalculator.divide(a, b)
}
}
非同期
interface AsyncArithmeticCalculator {
suspend fun add(a: Int, b: Int): Int
suspend fun subtract(a: Int, b: Int): Int
suspend fun multiply(a: Int, b: Int): Int
suspend fun divide(a: Int, b: Int): Int
}
class AsyncCalculator(
private val arithmeticCalculator: AsyncArithmeticCalculator
) {
suspend fun add(a: Int, b: Int): Int {
return arithmeticCalculator.add(a, b)
}
suspend fun subtract(a: Int, b: Int): Int {
return arithmeticCalculator.subtract(a, b)
}
suspend fun multiply(a: Int, b: Int): Int {
return arithmeticCalculator.multiply(a, b)
}
suspend fun divide(a: Int, b: Int): Int {
return arithmeticCalculator.divide(a, b)
}
}
ユニットテスト
実装クラスがなくてもテスト自体は書ける
同期
class CalculatorTest {
// ArithmeticCalculatorのスタブを作る
private val mockArithmeticCalculator = mockk<ArithmeticCalculator>()
private lateinit var calculator: Calculator
@Before
fun setUp() {
// スタブを使用してCalculatorクラスをインスタンス化
calculator = Calculator(mockArithmeticCalculator)
}
@Test
fun add() {
val expected = 3
// everyを使用して何をしたら何を返すかを決める
every {
mockArithmeticCalculator.add(1, 2)
} returns expected
assertTrue(calculator.add(1, 2) == expected)
}
@Test
fun zeroDivideThrowError() {
// エラーを出したい時はreturnsではなくthrowsを使用する
every {
mockArithmeticCalculator.divide(1, 0)
} throws ArithmeticException()
try {
calculator.divide(1, 0)
} catch (e: Exception) {
assertTrue(e is ArithmeticException)
}
// verifyを使用してdivideが呼び出されたかどうかを確認する
verify { mockArithmeticCalculator.divide(1, 0) }
}
}
非同期
ほとんど同じだが、runBlockingの中でcoEveryやcoVerifyを使用することでテストが可能になる
class AsyncCalculatorTest {
private val mockArithmeticCalculator = mockk<AsyncArithmeticCalculator>()
private lateinit var calculator: AsyncCalculator
@Before
fun setUp() {
calculator = AsyncCalculator(mockArithmeticCalculator)
}
@Test
fun add() = runBlocking {
val expected = 3
coEvery {
mockArithmeticCalculator.add(1, 2)
} returns expected
assertTrue(calculator.add(1, 2) == expected)
}
@Test
fun divide() = runBlocking {
coEvery {
mockArithmeticCalculator.divide(1, 0)
} throws ArithmeticException()
try {
calculator.divide(1, 0)
} catch (e: Exception) {
assertTrue(e is ArithmeticException)
}
coVerify { mockArithmeticCalculator.divide(1, 0) }
}
}
余談:Roomのユニットテストを書いてみる
Roomの準備
@Entity(tableName = "repository")
data class Repository(
@PrimaryKey
val id: Long,
val name: String,
val description: String,
@ColumnInfo(index = true)
val owner: String
)
@Dao
interface RepositoryDao {
@Insert
fun insertAll(vararg repositories: Repository)
@Query("SELECT * FROM repository WHERE owner = :owner")
fun findByOwner(owner: String): List<Repository>
}
@Database(entities = [Repository::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun repositoryDao(): RepositoryDao
}
class RepositoryLocalDataSource(val db: AppDatabase) {
fun insertAll(vararg repositories: Repository) {
db.repositoryDao().insertAll(*repositories)
}
fun findByOwner(owner: String): List<Repository> {
return db.repositoryDao().findByOwner(owner)
}
}
Roomのユニットテスト
実際のRoomを使ってローカルDBに保存されているかどうかという観点はインテグレーションテストの方が適当なので、ここではメソッドが1回だけ呼ばれたかどうかを判定する
class RepositoryLocalDataSourceTest {
private val mockDb: AppDatabase = mockk()
private lateinit var repositoryLocalDataSource: RepositoryLocalDataSource
@Before
fun setUp() {
repositoryLocalDataSource = RepositoryLocalDataSource(mockDb)
}
@Test
fun insertAll() {
val repository = Repository(1, "name", "description", "owner")
// just Runsで何も返さないことを指定する
every { repositoryLocalDataSource.insertAll(any()) } just Runs
repositoryLocalDataSource.insertAll(repository)
// exactly = 1で1回だけ呼び出されたかどうか判定する
verify(exactly = 1) { repositoryLocalDataSource.insertAll(repository) }
}
}
参考文献
Discussion