Androidアプリ開発の覚書き

Androidアプリ開発を、Kotlin, Jetpack Composeで行っています。気になったことをChatGPTに聞いて記事にしています。

Mockito:Kotlinテストでモックオブジェクトを用いる

KotlinでMockitoを使う方法を詳しく聞いてみました。

Kotlinのクラスはデフォルトでfinalクラスであるため、通常のクラスはMockitoで扱えません。そこで、依存関係をmockito-coreに代えてmockito-inlineを使用する必要があります。話の流れで最後の方で聞きましたが、むしろ最初に留意すべき点です。

後は、Mockitoで検証できる振る舞いをいろんな側面から聞きました。

メソッドの戻り値だけでなく、モックオブジェクトの呼び出し回数、特定の例外を投げるか、など、さまざまなことを検証できます。

Mockitoの使い方を詳しく教えてください。コードはKotlinでお願いします。

User
Mockitoの使い方を詳しく教えてください。コードはKotlinでお願いします。

ChatGPT
Mockitoは、JavaとKotlinのためのモックフレームワークです。テストで使用されることが多く、外部依存を模倣してテストの範囲を狭めることができます。ここでは、基本的なMockitoの使い方をKotlinで説明します。

Mockitoのセットアップ
まずは、プロジェクトにMockitoを追加する必要があります。Gradleを使用している場合、build.gradleファイルに以下の依存関係を追加します:

dependencies {  
  testImplementation "org.mockito:mockito-core:4.5.1"  
  testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"  
}  

基本的なモックの作成
次に、Mockitoを使用してモックオブジェクトを作成し、そのモックを使用してメソッドの戻り値を定義します。以下に例を示します。

import org.mockito.Mockito.mock  
import org.mockito.kotlin.whenever  
import org.junit.jupiter.api.Test  
import org.junit.jupiter.api.Assertions.assertEquals  
  
// インターフェースの定義  
interface MyService {  
  fun getData(): String  
}  
  
// テストクラス  
class MyServiceTest {  
  
  @Test  
  fun testGetData() {  
    // モックの作成  
    val mockService = mock(MyService::class.java)  
  
    // モックの振る舞いを定義  
    whenever(mockService.getData()).thenReturn("Mocked Data")  
  
    // モックの振る舞いをテスト  
    val result = mockService.getData()  
    assertEquals("Mocked Data", result)  
  }  
}  

モックの詳細な設定
Mockitoでは、メソッドの呼び出し回数を検証したり、例外を投げさせたりといった、より詳細なテスト設定が可能です。

import org.mockito.kotlin.verify  
import org.mockito.kotlin.times  
  
@Test  
fun testMethodInvocation() {  
  val mockService = mock(MyService::class.java)  
  
  // モックの振る舞いを定義  
  whenever(mockService.getData()).thenReturn("Mocked Data")  
  
  // メソッドを呼び出す  
  mockService.getData()  
  mockService.getData()  
  
  // 指定した回数だけメソッドが呼ばれたか検証  
  verify(mockService, times(2)).getData()  
}  

これで、基本的なMockitoの使い方とKotlinでのテストコードの書き方をご理解いただけたかと思います。特定の機能やより複雑な使用例について知りたい場合は、お知らせください。

verify()の内部的振る舞いや、mockServiceについて他に検証できることを教えてください。

User
verify()の内部的振る舞いや、mockServiceについて他に検証できることを教えてください。

あと、最後にverifyの戻り値に対してgetData()を呼び出しているのはなぜですか?

ChatGPT
verify() の内部的な振る舞い
verify() メソッドは、Mockitoで用意されているモックの振る舞いや呼び出し状況を検証するための手段です。このメソッドは指定されたモックが、指定された条件で呼び出されたかどうかを検証するために使います。内部的には、モックに関連付けられた呼び出し履歴を検証し、テストの仕様に合わせてその振る舞いが期待通りかを確認します。

例えば、verify(mockService, times(2)).getData() は mockService.getData() が正確に2回呼び出されたかを検証します。この確認を行う過程で、期待される回数と異なる場合はテストが失敗し、適切なエラーメッセージが生成されます。

○mockService に対して検証できること
mockService に関して検証できることには、以下のようなものがあります:

メソッド呼び出し回数: times() を使って特定のメソッドが期待通りの回数だけ呼ばれたかを検証できます。

メソッドの未呼び出し: never() を使って特定のメソッドが一度も呼ばれていないことを検証できます。

順序による呼び出し: InOrder を使って複数のメソッド呼び出しが特定の順序で行われたかを検証できます。

引数による検証: argThat() や eq() などを使って、メソッドに渡された引数が特定の条件を満たしているかを検証できます。

例外の投げ: thenThrow() を使ってモックが特定の条件下で例外を投げるように設定し、それが正しく機能するかを検証できます。

○verify の戻り値について
実際のところ、verify(mockService, times(2)).getData() の .getData() の部分はモックのメソッド呼び出しを指定するために使われているだけで、verify() の戻り値を利用しています。これはモックのそのメソッドが指定された条件(この場合は2回の呼び出し)に合致するかを検証するためのものであり、戻り値として何かを返しているわけではありません。つまり、これはモックの振る舞いを指定し、その振る舞いが期待通りに行われたかをテストするための表記方法です。

「モックのメソッド呼び出しを指定する」とはどういうことですか?

User
「モックのメソッド呼び出しを指定する」とはどういうことですか?

ChatGPT
「モックのメソッド呼び出しを指定する」という表現は、Mockitoで検証や振る舞いの定義を行う際に、どのメソッドに対して操作を行うかを指定するための表記法を指します。具体的には、verify() や when() などのメソッドを使ってモックオブジェクトの特定のメソッドが対象となるようにします。

たとえば、Mockitoでは以下のようにコードを書きます:

verify(mockService).getData()  

この例では、verify() 関数はモックオブジェクト mockService の特定のメソッド呼び出しを検証するために使われています。.getData() は mockService に対してどのメソッドの呼び出しを検証するかを指定しています。つまり、このコードは「mockService の getData() メソッドが呼ばれたことを検証する」という意味になります。

このように、Mockitoではメソッドチェーンを利用してモックの特定のメソッドに対する操作を簡潔に記述できます。これは内部的にモックの呼び出し履歴を追跡し、指定されたメソッドが呼び出されたかどうかを確認する機能に基づいています。

勘違いしていました。MockServiceのgetData()メソッドの呼び出し回数を検証する記法だったのですね。InOrder, argThat() や eq(), thenThrow()の使い方も教えてください。

User
勘違いしていました。MockServiceのgetData()メソッドの呼び出し回数を検証する記法だったのですね。

InOrder, argThat() や eq(), thenThrow()の使い方も教えてください。

ChatGPT
Mockitoを使って、より詳細な検証や条件付きの振る舞いを定義する方法について説明します。Kotlinでの例を用いて、InOrder, argThat(), eq(), そして thenThrow() の使い方を見ていきましょう。

InOrder
InOrder は複数のメソッド呼び出しが特定の順序で行われたことを検証するために使います。これは、メソッドの呼び出し順序が重要な場合に便利です。

import org.mockito.InOrder  
import org.mockito.Mockito.inOrder  
  
class MyServiceTest {  
  
  @Test  
  fun testMethodOrder() {  
    val mockService = mock(MyService::class.java)  
    val inOrder = inOrder(mockService)  
  
    mockService.getData()  
    mockService.setData("New Data")  
  
    // 順序を検証  
    inOrder.verify(mockService).getData()  
    inOrder.verify(mockService).setData("New Data")  
  }  
}  

argThat()
argThat() は特定の引数に対する検証を行います。カスタムの条件を定義して、その条件に合致するかどうかを確認します。

import org.mockito.kotlin.argThat  
  
class MyServiceTest {  
  
  @Test  
  fun testArgThat() {  
    val mockService = mock(MyService::class.java)  
  
    mockService.setData("Test")  
  
    verify(mockService).setData(argThat { it.length > 3 })  
  }  
}  

eq()
eq() は引数が特定の値と等しいかを検証する際に使用します。これは argThat() よりも具体的な値に対して使われます。

import org.mockito.kotlin.eq  
  
class MyServiceTest {  
  
  @Test  
  fun testEq() {  
    val mockService = mock(MyService::class.java)  
  
    mockService.setData("Test")  
  
    verify(mockService).setData(eq("Test"))  
  }  
}  

thenThrow()
thenThrow() はモックが特定の条件で例外を投げるように設定するために使用します。これはエラー処理をテストする際に役立ちます。

import org.mockito.kotlin.whenever  
  
class MyServiceTest {  
  
  @Test  
  fun testThenThrow() {  
    val mockService = mock(MyService::class.java)  
  
    whenever(mockService.getData()).thenThrow(RuntimeException("Error"))  
  
    assertThrows<RuntimeException> {  
      mockService.getData()  
    }  
  }  
}  

これらのメソッドを使用することで、モックオブジェクトの振る舞いを細かくコントロールし、アプリケーションの様々な側面を効率的にテストすることができます。

ありがとうございます。すごく便利ですね。whenever()と同系統のメソッドについても教えてください。

User
ありがとうございます。すごく便利ですね。

whenever()と同系統のメソッドについても教えてください。

ChatGPT
whenever() メソッドは、MockitoのKotlin拡張ライブラリにおける機能で、モックの振る舞いを定義する際に使用されます。これに類似する他の方法や機能についても説明します。これらはモックの動作を設定し、テストの状況をシミュレートするために役立ちます。

1. when()
JavaのMockitoでは、when() メソッドが whenever() と同様に使われます。これを使ってモックの特定のメソッドが呼ばれたときの戻り値や振る舞いを指定できます。

import static org.mockito.Mockito.*;  
  
public class MyServiceTest {  
  @Test  
  public void testGetData() {  
    MyService mockService = mock(MyService.class);  
  
    // モックの振る舞いを定義  
    when(mockService.getData()).thenReturn("Mocked Data");  
  
    assertEquals("Mocked Data", mockService.getData());  
  }  
}  

2. doReturn() / doThrow() / doAnswer() / doNothing()
これらのメソッドは、when() や whenever() と同じくモックの振る舞いを定義するために使われますが、特に副作用を持つメソッドやスパイ(部分的なモック)の設定時に便利です。

doReturn():特定の値を返すように設定します。

doThrow():特定の例外を投げるように設定します。

doAnswer():より複雑な振る舞いや副作用を持たせる時に使います。

doNothing():何もしない(通常はvoidメソッドで使用)。

import org.mockito.kotlin.doReturn  
import org.mockito.kotlin.doThrow  
import org.mockito.kotlin.mock  
  
class MyServiceTest {  
  
  @Test  
  fun testComplexBehavior() {  
    val mockService = mock<MyService>()  
  
    // 条件に応じて異なる振る舞いを設定  
    doReturn("Data").`when`(mockService).getData()  
    doThrow(RuntimeException("Error")).`when`(mockService).setData(anyString())  
  }  
}  

3. thenReturn()
thenReturn() は when() や whenever() と一緒に使用して、メソッドが返すべき値を指定します。連続して異なる値を返す設定も可能です。

import org.mockito.kotlin.whenever  
  
class MyServiceTest {  
  
  @Test  
  fun testMultipleReturns() {  
    val mockService = mock<MyService>()  
  
    whenever(mockService.getData())  
      .thenReturn("First call")  
      .thenReturn("Second call")  
      .thenReturn("Third call")  
  
    assertEquals("First call", mockService.getData())  
    assertEquals("Second call", mockService.getData())  
    assertEquals("Third call", mockService.getData())  
  }  
}  

これらのメソッドを適切に使い分けることで、テストのニーズに合わせてモックの振る舞いを柔軟に定義することができます。

doReturn, doThrowの使い方がよく分かりません。

User
doReturn, doThrowの使い方がよく分かりません。

ChatGPT
MockitoでのdoReturn()やdoThrow()の使い方について詳しく説明します。これらのメソッドはwhen()やwhenever()とは異なり、メソッドチェーンの順序が逆になります。これにより、モックの設定をより柔軟に行うことができるため、voidメソッドや例外を扱う際などに特に便利です。

doReturn()
doReturn()はモックが特定の値を返すように設定するために使用します。これはwhen()と異なり、副作用が発生する可能性のあるコードを実行する前にモックの振る舞いを設定するのに適しています(例えば、nullが返されるとNullPointerExceptionが発生するような場合)。

使い方

import org.mockito.kotlin.doReturn  
import org.mockito.kotlin.mock  
  
class MyServiceTest {  
  @Test  
  fun testDoReturn() {  
    val mockService = mock<MyService>()  
  
    // ここでは doReturn() を使って、特定のメソッド呼び出しに対する戻り値を設定します。  
    doReturn("Mocked Response").`when`(mockService).getData()  
  
    val result = mockService.getData()  
    assertEquals("Mocked Response", result)  
  }  
}  

doThrow()
doThrow()はモックが特定の条件下で例外を投げるように設定するために使用します。これはテスト中にエラーハンドリングを適切に行っているかを確認するのに役立ちます。

使い方

import org.mockito.kotlin.doThrow  
import org.mockito.kotlin.mock  
import kotlin.test.assertFailsWith  
  
class MyServiceTest {  
  @Test  
  fun testDoThrow() {  
    val mockService = mock<MyService>()  
  
    // doThrow() を使用して、メソッドが呼ばれた際に例外を投げるように設定  
    doThrow(RuntimeException("Forced Error")).`when`(mockService).setData(any())  
  
    // 例外が投げられることを検証  
    assertFailsWith<RuntimeException> {  
      mockService.setData("Some Data")  
    }  
  }  
}  

これらのメソッドを使うことで、モックの振る舞いを非常に詳細に制御し、様々なテストシナリオをシミュレートすることが可能になります。特に、テストケースで例外の発生を検証したい場合や、返り値の設定が複雑な場合に有効です。

ありがとうございます。順序を変えたいときに使えばよいのですね。引数の指定を行うanything()と同系統のメソッドについても教えてください。

User
ありがとうございます。順序を変えたいときに使えばよいのですね。

引数の指定を行うanything()と同系統のメソッドについても教えてください。

ChatGPT
Mockitoで引数の指定や一致を行うために使用するメソッドはいくつかあります。これらはテストの柔軟性を高め、特定の条件でのみモックの振る舞いを制御するのに役立ちます。ここでは、any() (Kotlinでは any() が anything() に相当) と同系統の一般的なメソッドについて説明します。

any()
any() メソッドは、どんなタイプや値の引数でも一致すると判断します。これはテスト中に特定の引数の値が重要でない場合に便利です。

import org.mockito.kotlin.any  
import org.mockito.kotlin.whenever  
  
whenever(mockService.getData(any())).thenReturn("Mocked Data")  

eq()
eq() メソッドは、引数が特定の値と正確に一致する必要がある場合に使用します。これは any() よりも厳密な一致を要求します。

import org.mockito.kotlin.eq  
import org.mockito.kotlin.whenever  
  
whenever(mockService.getData(eq("Specific data"))).thenReturn("Mocked Data")  

argThat()
argThat() はカスタムの条件で引数を一致させたい場合に使用します。引数が特定の条件を満たすかどうかをチェックするためのカスタムマッチャーを提供します。

import org.mockito.kotlin.argThat  
  
whenever(mockService.getData(argThat { it.length > 5 })).thenReturn("Mocked Data")  

anyOrNull()
anyOrNull() は any() と似ていますが、null 値も許容されます。このマッチャーは、引数が null であるかどうかに依存しない場合に便利です。

import org.mockito.kotlin.anyOrNull  
  
whenever(mockService.getData(anyOrNull())).thenReturn("Mocked Data")  

isA()
isA() は引数が特定の型であることを検証する際に使用します。指定されたクラスのインスタンスである引数のみが一致します。

import org.mockito.kotlin.isA  
  
whenever(mockService.processData(isA<String>())).thenReturn("Processed")  

これらのメソッドを使用することで、モックの振る舞いを引数の値やタイプに基づいて詳細に制御することができます。テストを書く際にこれらのツールを適切に使い分けることで、より堅牢で信頼性の高いテストケースを作成することが可能になります。

Mockには、Kotlinのクラスも使えるのですか?

User
Mockするには、Kotlinのクラスも使えるのですか?

ChatGPT
はい、KotlinのクラスもMockitoを使ってモックすることができます。KotlinでのクラスやインターフェイスのモッキングはJavaと非常に似ており、同様のメソッドとアプローチが使えます。ただし、Kotlinのいくつかの特性により、Javaと異なる考慮が必要な場合があります。ここでいくつかのポイントを説明します。

Kotlinのクラスをモックする際のポイント
オープンクラス: Kotlinでは、デフォルトで全てのクラスが final です。これは、そのクラスが継承できないことを意味し、通常の方法でモックを作成できないことを意味します。そのため、モックを作成するには、クラスを open クラスにする必要があります。テストのために多くのクラスを open にするのは望ましくないため、Mockitoの inline-mock-maker プラグインを使用するか、オープンクラスを生成するためのライブラリ(例えばAll-openプラグイン)の使用を検討することが推奨されます。

インターフェース: インターフェースは final ではないので、簡単にモックできます。そのため、インターフェースに依存する設計(依存性の逆転など)を使うことで、テストが容易になります。

コンストラクタのパラメータ: Kotlinのプライマリコンストラクタはしばしばデフォルト引数を持ちます。Mockitoでは、コンストラクタのパラメータを持つクラスをモックする際に、これらのデフォルト値を適切に扱う必要があります。これには、適切なコンストラクタを選択するか、または mock メソッドで適切な引数を提供することが含まれます。

例:Kotlinクラスのモック

import org.mockito.kotlin.mock  
import org.mockito.kotlin.whenever  
import org.junit.jupiter.api.Test  
import org.junit.jupiter.api.Assertions.assertEquals  
  
// このクラスをモックするためには、openを付ける必要がある  
open class DataService {  
  open fun getData(): String = "Real Data"  
}  
  
class DataServiceTest {  
  
  @Test  
  fun testGetData() {  
    // DataServiceのモックを作成  
    val mockService = mock<DataService>()  
  
    // モックの振る舞いを定義  
    whenever(mockService.getData()).thenReturn("Mocked Data")  
  
    // モックの振る舞いをテスト  
    assertEquals("Mocked Data", mockService.getData())  
  }  
}  

この例では、DataService クラスが open であり、その getData() メソッドも open です。これにより、Mockitoを使用してモックを作成し、その振る舞いを定義できます。

Kotlinでクラスをモックする際には、これらのポイントを考慮することが重要です。適切な設計とツールの使用により、Kotlinのクラスを効果的にモックできます。

Mockitoの inline-mock-maker プラグインの使い方も教えてください。

User
Mockitoの inline-mock-maker プラグインの使い方も教えてください。

ChatGPT
Mockitoの inline-mock-maker プラグインは、KotlinやJavaの final クラスやメソッドをモックするために特に有用です。通常、Mockitoでは final なクラスやメソッドをモックすることはできませんが、このプラグインを使うことでそれが可能になります。以下にその設定方法を説明します。

1. プラグインの設定
Mockitoのバージョン2.1.0以降で inline-mock-maker プラグインを有効にするには、以下のステップに従います:

Gradleを使用している場合:
依存関係の追加:
Mockitoの mockito-inline 依存性をプロジェクトの build.gradle ファイルに追加します。

dependencies {  
  testImplementation "org.mockito:mockito-inline:4.5.1" // Mockitoの最新バージョンを確認してください  
}  

この依存関係は、標準の mockito-core 依存関係に代わるものです。両方を同時に含めないように注意してください。

プラグインの設定:
mockito-inline は追加するだけで自動的に設定され、特別な設定ファイルを必要としません。

Mavenを使用している場合:
依存関係の追加:
pom.xml に mockito-inline 依存性を追加します。

<dependencies>  
  <dependency>  
    <groupId>org.mockito</groupId>  
    <artifactId>mockito-inline</artifactId>  
    <version>4.5.1</version> <!-- Mockitoの最新バージョンを確認してください -->  
    <scope>test</scope>  
  </dependency>  
</dependencies>  

2. プラグインの使用
mockito-inline を設定した後、テスト内で通常どおりモックを作成し、 final クラスやメソッドもモックできるようになります。

import org.mockito.kotlin.mock  
import org.mockito.kotlin.whenever  
import org.junit.jupiter.api.Test  
import org.junit.jupiter.api.Assertions.assertEquals  
  
// final クラスの例  
final class FinalDataService {  
  fun getData(): String = "Real Data"  
}  
  
class FinalDataServiceTest {  
  
  @Test  
  fun testGetData() {  
    // FinalDataServiceのモックを作成  
    val mockService = mock<FinalDataService>()  
  
    // モックの振る舞いを定義  
    whenever(mockService.getData()).thenReturn("Mocked Data")  
  
    // モックの振る舞いをテスト  
    assertEquals("Mocked Data", mockService.getData())  
  }  
}  

この設定により、final 修飾子が付いたクラスやメソッドでも自由にモックを作成し、テストを行うことができるようになります。これにより、Kotlinでのテストが格段に簡単になります。

User
ありがとうございました。

ChatGPT
どういたしまして!また何か質問があればいつでもお気軽にどうぞ。お役に立ててうれしいです!