📱

Android Test Mockk 入門

2024/06/27に公開

Mockkはテストダブルを作るためのライブラリ

他に有名なものにmockitoがある。

今回はMockkがKotlinベースで作られていることCoroutinesをサポートしていることからMockkの使い方をメモしていく

基本

依存の追加

testImplementation("io.mockk:mockk:1.13.11")

https://github.com/mockk/mockk

テストの準備

同期的なクラスと非同期的なクラスを用意する

同期

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) }
    }
}

参考文献

https://zenn.dev/okuzawats/books/android-unit-testing/viewer/mockk

Discussion