1. 概要

このチュートリアルでは、MockKライブラリの基本的な機能のいくつかを見ていきます。

2. MockK

Kotlin では、すべてのクラスとメソッドがfinalです。 これは不変のコードを書くのに役立ちますが、テスト中にいくつかの問題を引き起こします。

ほとんどのJVMモックライブラリには、最終クラスのモックまたはスタブに関する問題があります。 もちろん、モックしたいクラスやメソッドに「open」キーワードを追加することもできます。 しかし、一部のコードをモックするためだけにクラスを変更することは、最善のアプローチとは思えません。

Kotlin言語の機能と構成をサポートするMockKライブラリが登場します。 MockKは、モックされたクラスのプロキシを構築します。 これによりパフォーマンスがいくらか低下しますが、MockKがもたらす全体的なメリットはそれだけの価値があります。

3. インストール

インストールは可能な限り簡単です。 mockk依存関係をMavenプロジェクトに追加する必要があります。

<dependency>
    <groupId>io.mockk</groupId>
    <artifactId>mockk</artifactId>
    <version>1.9.3</version>
    <scope>test</scope>
</dependency>

Gradle の場合、テスト依存関係として追加する必要があります。

testImplementation "io.mockk:mockk:1.9.3"

4. 基本例

モックしたいサービスを作成しましょう。

class TestableService {
    fun getDataFromDb(testParameter: String): String {
        // query database and return matching value
    }

    fun doSomethingElse(testParameter: String): String {
        return "I don't want to!"
    }
}

TestableServiceをモックするテストの例を次に示します。

@Test
fun givenServiceMock_whenCallingMockedMethod_thenCorrectlyVerified() {
    // given
    val service = mockk<TestableService>()
    every { service.getDataFromDb("Expected Param") } returns "Expected Output"
 
    // when
    val result = service.getDataFromDb("Expected Param")
 
    // then
    verify { service.getDataFromDb("Expected Param") }
    assertEquals("Expected Output", result)
}

モックオブジェクトを定義するために、 mockk <…>() 方法。

次のステップでは、モックの動作を定義しました。 この目的のために、どの呼び出しに対してどの応答が返されるかを記述するeveryブロックを作成しました。

最後に、 verify ブロックを使用して、モックが期待どおりに呼び出されたかどうかを確認しました

5. 注釈の例

MockKアノテーションを使用して、あらゆる種類のモックを作成することができます。 TestableServiceの2つのインスタンスを必要とするサービスを作成しましょう。

class InjectTestService {
    lateinit var service1: TestableService
    lateinit var service2: TestableService

    fun invokeService1(): String {
        return service1.getDataFromDb("Test Param")
    }
}

InjectTestService には、同じタイプの2つのフィールドが含まれています。 MockKにとっては問題にはなりません。 MockKは、プロパティを名前で照合し、次にクラスまたはスーパークラスで照合しようとします。 また、は、プライベートフィールドへのオブジェクトの挿入に問題はありません。

アノテーションを使用して、テストでInjectTestServiceをモックしてみましょう。

class AnnotationMockKUnitTest {

    @MockK
    lateinit var service1: TestableService

    @MockK
    lateinit var service2: TestableService

    @InjectMockKs
    var objectUnderTest = InjectTestService()

    @BeforeEach
    fun setUp() = MockKAnnotations.init(this)

    // Tests here
    ...
}

上記の例では、 @InjectMockKs annotation を使用しました。これは、定義されたモックが注入されるオブジェクトを指定します。 デフォルトでは、まだ割り当てられていない変数を挿入します。 @OverrideMockKs を使用して、すでに値を持つフィールドをオーバーライドできます。

MockKでは、アノテーション付きの変数を宣言するオブジェクトでMockKAnnotations.init(…)を呼び出す必要があります。 Junit5 の場合、 @ExtendWith(MockKExtension :: class )

6. スパイ

Spyでは、特定のクラスの特定の部分のみをモックできます。たとえば、 TestableService:の特定のメソッドをモックするために使用できます。

@Test
fun givenServiceSpy_whenMockingOnlyOneMethod_thenOtherMethodsShouldBehaveAsOriginalObject() {
    // given
    val service = spyk<TestableService>()
    every { service.getDataFromDb(any()) } returns "Mocked Output"
 
    // when checking mocked method
    val firstResult = service.getDataFromDb("Any Param")
 
    // then
    assertEquals("Mocked Output", firstResult)
 
    // when checking not mocked method
    val secondResult = service.doSomethingElse("Any Param")
 
    // then
    assertEquals("I don't want to!", secondResult)
}

この例では、spykメソッドを使用してスパイオブジェクトを作成しました。 @SpyK アノテーションを使用して、同じことを実現することもできます。

class SpyKUnitTest {

    @SpyK
    lateinit var service: TestableService

    // Tests here
}

7. リラックスしたモック

戻り値が指定されていないメソッドを呼び出そうとすると、一般的なモックオブジェクトはMockKExceptionをスローします。

各メソッドの動作を説明したくない場合は、リラックスしたモックを使用できます。この種類のモックは、各関数のデフォルト値を提供します。 たとえば、 String 戻りタイプは、空のStringを返します。 簡単な例を次に示します。

@Test
fun givenRelaxedMock_whenCallingNotMockedMethod_thenReturnDefaultValue() {
    // given
    val service = mockk<TestableService>(relaxed = true)
 
    // when
    val result = service.getDataFromDb("Any Param")
 
    // then
    assertEquals("", result)
}

この例では、mockkメソッドとrelaxed属性を使用して、リラックスしたモックオブジェクトを作成しました。 @RelaxedMockKアノテーションを使用することもできます。

class RelaxedMockKUnitTest {

    @RelaxedMockK
    lateinit var service: TestableService

    // Tests here
}

8. オブジェクトモック

Kotlinは、objectキーワードを使用してシングルトンを宣言する簡単な方法を提供します。

object TestableService {
    fun getDataFromDb(testParameter: String): String {
        // query database and return matching value
    }
}

ただし、ほとんどのモックライブラリには、Kotlinシングルトンインスタンスのモックに問題があります。 このため、MockKはmockkObjectメソッドを提供します。 見てみましょう:

@Test
fun givenObject_whenMockingIt_thenMockedMethodShouldReturnProperValue(){
    // given
    mockkObject(TestableService)
 
    // when calling not mocked method
    val firstResult = service.getDataFromDb("Any Param")
 
    // then return real response
    assertEquals(/* DB result */, firstResult)

    // when calling mocked method
    every { service.getDataFromDb(any()) } returns "Mocked Output"
    val secondResult = service.getDataFromDb("Any Param")
 
    // then return mocked response
    assertEquals("Mocked Output", secondResult)
}

9. 階層的モッキング

MockKのもう1つの便利な機能は、階層オブジェクトをモックする機能です。 まず、階層オブジェクト構造を作成しましょう。

class Foo {
    lateinit var name: String
    lateinit var bar: Bar
}

class Bar {
    lateinit var nickname: String
}

The フークラスにはタイプのフィールドが含まれていますバー。 これで、たった1つの簡単なステップで構造をモックできます。 nameフィールドとnicknameフィールドをモックしてみましょう。

@Test
fun givenHierarchicalClass_whenMockingIt_thenReturnProperValue() {
    // given
    val foo = mockk<Foo> {
        every { name } returns "Karol"
        every { bar } returns mockk {
            every { nickname } returns "Tomato"
        }
    }
 
    // when
    val name = foo.name 
    val nickname = foo.bar.nickname
 
    // then
    assertEquals("Karol", name)
    assertEquals("Tomato", nickname)
}

10. パラメータのキャプチャ

メソッドに渡されたパラメーターをキャプチャする必要がある場合は、CaptureSlotまたはMutableListを使用できます。answer[にカスタムロジックを含める場合に便利です。 X189X]ブロック、または渡された引数の値を確認する必要があります。 CaptureSlot:の例を次に示します。

@Test
fun givenMock_whenCapturingParamValue_thenProperValueShouldBeCaptured() {
    // given
    val service = mockk<TestableService>()
    val slot = slot<String>()
    every { service.getDataFromDb(capture(slot)) } returns "Expected Output"
 
    // when
    service.getDataFromDb("Expected Param")
 
    // then
    assertEquals("Expected Param", slot.captured)
}

MutableList を使用して、すべてのメソッド呼び出しの特定の引数値をキャプチャして保存できます。

@Test
fun givenMock_whenCapturingParamsValues_thenProperValuesShouldBeCaptured() {
    // given
    val service = mockk<TestableService>()
    val list = mutableListOf<String>()
    every { service.getDataFromDb(capture(list)) } returns "Expected Output"
 
    // when
    service.getDataFromDb("Expected Param 1")
    service.getDataFromDb("Expected Param 2")
 
    // then
    assertEquals(2, list.size)
    assertEquals("Expected Param 1", list[0])
    assertEquals("Expected Param 2", list[1])
}

11. 結論

この記事では、MockKの最も重要な機能について説明しました。 MockKは、Kotlin言語用の強力なライブラリであり、多くの便利な機能を提供します。 MockKの詳細については、MockKWebサイトドキュメントを確認してください。

いつものように、提示されたサンプルコードは、GitHubで入手できます。