ncmb_kotlin テスト構造について

### Kotlin SDKのテストについて

今回先日リリースしましたmobile backendの[Kotlin SDK](https://github.com/NIFCLOUD-mbaas/ncmb_kotlin)のテストについて紹介となります。KotlinのUnit testやモックサーバなどのことが中心にテストを追加するケースごとのサンプルの紹介も記載しています。SDKのテストってどんなことなのか、モックサーバーの使い方などご参考になればかと思います。ご意見がある方はぜひコメントもお待ちしております。

#### 利用するライブラリー

- テストでは以下のライブラリを利用しています。
 - JUnit
 - robolectric
 - okhttp3.mockwebserver

#### テストコード実行方法

- テストケースは、'src/test/java/com/nifcloud/mbaas/core/'フォルダー内に格納されています。
* Robolectricを利用するため、テストクラスの前に以下の記載が必要
```
@RunWith(RobolectricTestRunner::class)
```

また、テストメソッドの前に以下のように記載してください。

```
@Test
fun testAbc(){

}
```

* 実行する際、Android Studioでテスト実行を選択し、行います。ファイルごとに、メソッドごとに、フォルダーごとにどちらでも可能。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18698/4fe3f4d9-39a5-e19d-7d05-9a4f23801373.png)


#### Kotlin SDKのテスト構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18698/94b480d4-70bb-0fc5-7d1f-b42c3afb5540.png)


* モック制御する部分
  - ymlファイル：src/test/assets/yaml/mbaas***.yml リクエストレスポンスのマッピングファイル
  - 例
  
```
request:
    url: /2013-09-01/classes/TestClass
    method: POST
    body:
        key: value
response:
    status: 201
    file: valid_post_response.json
```

  - リクエストレスポンスファイル（複数）：src/test/assets/json(NCMBDispatcher用), src/test/assets/json_error(NCMBErrorDispatcher用)
  - 例

```
{"objectId":"7FrmPTBKSNtVjajm","createDate":"2014-06-03T11:28:30.348Z"}
```

  - メイン処理：ymlファイルを読み込み、受けているリクエストの情報とymlファイル設定情報を比較し、該当するレスポンスファイルをMockResponse()として返却します。
    - 正常リクエスト用NCMBDispatcher.kt(ncmb_kotlin/src/test/java/com/nifcloud/mbaas/core/)
    - 例外リクエスト用 NCMBErrorDispatcher.kt(ncmb_kotlin/src/test/java/com/nifcloud/mbaas/core/)

- その他
  - 非同期処理のcallbackをテストするためのhelperクラス：`ncmb_kotlin/src/test/java/com/nifcloud/mbaas/core/helper/NCMBInBackgroundTestHelper.kt` 
コールバック内の結果のチェックを実施するために、スレッドを一時的に止めるように処理します。

### テストを書く時に必要なこと

* API利用メソッドのテストに関しては、サーバモックを利用するために、
以下のようにモックを定義しています。

```
private var mServer: MockWebServer = MockWebServer()
```

* `@Before` 処理で、必要な初期化設定を行います

```kotlin
@Before
    fun setup() {
        val ncmbDispatcher = NCMBDispatcher()
        mServer.dispatcher = ncmbDispatcher
        mServer.start()
        NCMB.initialize(
            RuntimeEnvironment.application.getApplicationContext(),
            "appKey",
            "cliKey",
            mServer.url("/").toString(),
            "2013-09-01"
        )

        callbackFlag = false;
    }
```
- 同期処理テスト

```kotlin
    @Test
    @Throws(NCMBException::class)
    fun save_object_with_post_data_save_success() {
        var obj = NCMBObject("TestClass")
        obj.put("key", "value")
        obj = obj.save()
        Assert.assertEquals(obj.getObjectId(), "7FrmPTBKSNtVjajm")
    }
```

- 非同期処理

```kotlin
    @Test
    fun saveInBackground_object_with_post_data_save_success() {
        val inBackgroundHelper = NCMBInBackgroundTestHelper() // ヘルパーの初期化
        val obj = NCMBObject("TestClass")
        val callback = NCMBCallback { e, ncmbObj ->
            inBackgroundHelper["e"] = e
            inBackgroundHelper["ncmbObj"] = ncmbObj
            inBackgroundHelper.release() // ブロックをリリース
        }
        obj.put("key", "value")
        inBackgroundHelper.start()
        obj.saveInBackground(callback)
        inBackgroundHelper.await()
        print("Success saved: ObjectID " + (inBackgroundHelper["ncmbObj"] as NCMBObject).getObjectId() + " | Response data: CreateDate " + (inBackgroundHelper["ncmbObj"] as NCMBObject).getCreateDate())
        Assert.assertTrue(inBackgroundHelper.isCalledRelease())
        Assert.assertNull(inBackgroundHelper["e"])
        Assert.assertEquals((inBackgroundHelper["ncmbObj"] as NCMBObject).getObjectId(), "7FrmPTBKSNtVjajm")
        val date: Date = getIso8601().parse("2014-06-03T11:28:30.348Z")!!
        Assert.assertEquals((inBackgroundHelper["ncmbObj"] as NCMBObject).getCreateDate(), date)
    }
```

#### エラーのテスト

* ケース1：SDKの処理内のエラー

```kotlin
 @Test
    fun test_geopoint_wrong_latitude(){
        try {
            val latitude: Double = 139.765607
            val longitude: Double = 139.765607
            val geopoint = NCMBGeoPoint(latitude, longitude)
        }
        catch(e: NCMBException){
            Assert.assertEquals(e.message, "set the latitude to a value between -90 and 90")
        }
    }

```

* ケース2：同期処理のエラー

```
    @Test
    @Throws(Exception::class)
    fun testNCMBObject_DoSearchSync_503error() {
        //TestClassクラスを検索するクエリを作成
        val query = NCMBQuery.forObject("TestClass503")
        val throwable = assertFails{ val objects = query.find() }
        Assert.assertEquals("Service unavailable.",throwable.message)
    }
```
* ケース3: 非同期処理のエラー

```kotlin
    @Test
    @Throws(java.lang.Exception::class)
    fun loginInBackground_invalid_username() {
        val inBackgroundHelper = NCMBInBackgroundTestHelper() // ヘルパーの初期化
        val callback = NCMBCallback { e, ncmbUser ->
            inBackgroundHelper["e"] = e
            inBackgroundHelper["ncmbUser"] = ncmbUser
            inBackgroundHelper.release() // ブロックをリリース
        }
        inBackgroundHelper.start()
        NCMBUser().loginInBackground("invalidUser", "Password", callback)
        inBackgroundHelper.await()
        Assert.assertTrue(inBackgroundHelper.isCalledRelease())
        Assert.assertNull(NCMBUser().getCurrentUser().getObjectId())
        Assert.assertEquals(NCMBException.AUTH_FAILURE, (inBackgroundHelper["e"] as NCMBException).code)
    }
```

#### 参考

https://medium.com/xebia-engineering/the-recommended-way-of-testing-http-calls-mockwebserver-by-okhttp-e716f87d6122