Activity以外のクラスでリソースを取得する

Activity以外のクラスで、文字列リソースを取得したいケースがあっていくつかハマりました。

## どこでハマったのか
たとえば、こんな感じでViewModelに依存するActivityがあって...

```
+-----------+    +-----------+
| Activity  |    | ViewModel |
+-----+-----+    +-----+-----+
      |                |
      |     greet()    |
      +--------------->|
      |<---------------+
      |      Hello     |
```
こんなコードになったとします。

### アクティビティ
```kotlin
class MainActivity : AppCompatActivity() {

    private val viewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        println("_/_/ ${this.viewModel.greet()}")
    }
}
```
### ViewModel
```kotlin
class MainViewModel {
    fun greet(): String {
        return "こんにちは"
    }
}
```

この場合、当然ViewModelからは`"こんにちは"`が返されます。

この `"こんにちは"`をリソースから取得しようとするとどうでしょう？
`getText()`とか`getString()`を使えば良いんですけど、こいつらContextが無いと呼べないんですよね...

「ViewModelにContext渡せば良いじゃん」と思うかもしれませんが、
それだとActivityが生成されないユニットテストの時に困ります。

ではどうしたかというと...

## 解決方法

私が思いついた解決方法は、**どこからでもリソースを参照できるようにする** というものです。

### アプリケーションクラス
アプリケーションクラスのインスタンスを`companion object`として定義した変数に保持しておきます。

```kotlin
class Application : android.app.Application() {

    companion object {
        lateinit var instance: Application private set  // <- これ
    }

    override fun onCreate() {
        super.onCreate()

        instance = this  // <- これ
    }
}
```
### マニフェスト
アプリをアプリケーションクラスから起動するようにします。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myresourceapp">

    <application
        android:name=".Application"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```
### リソース
```xml:string.xml
<resources>
    <string name="app_name">MyResourceApp</string>
    <string name="greeting">こんにちは</string>
</resources>
```
### ViewModel
```kotlin
class MainViewModel {
    fun greet(): String {
        return Application.instance.getString(R.string.greeting)
    }
}
```
### 結果
こんな感じでログが表示されました。
ViewModelからリソースの参照に成功しています。

```text:logcat
06-25 08:12:08.628 3617-3617/com.example.myresourceapp I/System.out: _/_/ こんにちは
```

## ユニットテスト
ではこのViewModelをテストしてみようと思います。

### テストコード
```kotlin
import org.hamcrest.core.Is.*
import org.junit.Assert
import org.junit.Test

class MainViewModelTest {
    @Test
    fun `greet()メソッドが "こんにちは" を返すこと`() {
        val viewModel = MainViewModel()
        Assert.assertThat(viewModel.greet(), `is`("こんにちは"))
    }
}
```

### 結果
テストが失敗して、こんなエラーが表示されました。

```text
kotlin.UninitializedPropertyAccessException: lateinit property instance has not been initialized
```

これ、アプリケーションクラスがインスタンス化されていないために、
`Application`に`lateinit`で定義した`instance`が初期化されていない。
と言っているみたいです。

それならばと、`app/src/test/`の下に`AndroidManifest.xml`をコピーしてみたけど駄目...

じゃあどうしようか...

## Robolectric
[Robolectric](http://robolectric.org/)というものを使いました。

これはなにかと言うと、ユニットテストのようなAndroid環境を使わないJVMテストにおいて、
AndroidのAPIを呼び出せるようにしたものらしいです。
ただ、Androidと同じ挙動をするというわけではなく、APIを呼び出してもエラーにならない程度みたいですね。

で、このRobolectricを使うと、Applicationをインスタンス化できます。

### Robolectricをプロジェクトに導入
```groovy:app/build.gradle
android {

    // 省略

    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
}

dependencies {

    // 省略

    testImplementation "org.robolectric:robolectric:3.8"
}
```
### テストコード
`@RunWith`と`@Config`を追加します。
`@Config`の`application`に指定したクラスが起動時にインスタンス化されるみたいです。

```kotlin
import org.hamcrest.core.Is.*
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)     // <- これ
@Config(application = Application::class)  // <- これ
class MainViewModelTest {
    @Test
    fun `greet()メソッドが "こんにちは" を返すこと`() {
        val viewModel = MainViewModel()
        Assert.assertThat(viewModel.greet(), `is`("こんにちは"))
    }
}
```
###　結果
`"Process finished with exit code 0"`になって、テストが成功しました！

```text
Downloading: org/robolectric/android-all/8.1.0-robolectric-4611349/android-all-8.1.0-robolectric-4611349.pom from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 2K from sonatype
Downloading: org/robolectric/android-all/8.1.0-robolectric-4611349/android-all-8.1.0-robolectric-4611349.jar from repository sonatype at https://oss.sonatype.org/content/groups/public/
Transferring 95237K from sonatype

Process finished with exit code 0
```

ただし、初回はRobolectricが100MBくらいのファイルをダウンロードしてるっぽいので時間がかかります。

## さらにハマる
これだけで済めばよかったのですが、アプリケーションクラスでAndroidのAPI以外を呼び出すことがあり、
例えば[Realm](https://realm.io/jp)を使っていたるすると...

```kotlin

import io.realm.Realm

class Application : android.app.Application() {

    companion object {
        lateinit var instance: Application private set
    }

    override fun onCreate() {
        super.onCreate()

        Realm.init(this)  // <- これ

        instance = this
    }
}
```
というような感じでRealmを初期化すると思いますが、
テストを実行すると`Can't load library`みたいなことを言われてエラーになります。

実は、これも回避方法があって...

```kotlin
class TestApplication : android.app.Application() {

    companion object {
        lateinit var instance: TestApplication private set
    }

    override fun onCreate() {
        super.onCreate()

        instance = this
    }
}
```
という感じで、Realmを初期化しないアプリケーションクラスを実装して、テストコードの`@Config`のとこをこんな風に書き換えます。

```kotlin
@Config(application = TestApplication::class)  // <- Application を TestApplication に変更
```
ここまで修正して、再びテストを実行すると...

```text
kotlin.UninitializedPropertyAccessException: lateinit property instance has not been initialized
```
エラーになってしまいました。

これはViewModelが

```kotlin
class MainViewModel {
    fun greet(): String {
        return Application.instance.getString(R.string.greeting)
    }
}
```
こうなっていて、`Application`クラスを参照しているからです。
これを`TestApplication`に変えたいです。
でもここを`TestApplication`に書き換えてしまう訳にもいかないので、テスト時だけ差し替わって欲しいです。

そういう時はどうするかと言うと...

```kotlin
object ResourceFinder {
    fun getString(@StringRes id: Int): String {
        return Application.instance.resources.getString(id)
    }
}
```
こういうクラスを作って、

```kotlin
class MainViewModel {
    fun greet(): String {
        return ResourceFinder.getString(R.string.greeting)
    }
}
```
ViewModelから利用します。
そして、こんなクラスも作ります。
`TestApplication`経由でリソースを取得します。

```kotlin
@Implements(ResourceFinder::class)
class ShadowResourceFinder {
    fun getString(@StringRes id: Int): String {
        return TestApplication.instance.resources.getString(id)
    }
}
```
`@Implements`というアノテーションで、
「`ResourceFinder`の代わりに`ShadowResourceFinder`を使いますよ」
と宣言しています。
`object`じゃなくて`class`にしておかないと駄目です。コンストラクタが見つからないとか言われます。

そして、テストコードの`@Config`に`shadows`というものを追加します。
配列にして代替クラスを指定します。

```kotlin
@RunWith(RobolectricTestRunner::class)
@Config(application = TestApplication::class, shadows = [ShadowResourceFinder::class]) // <- これ
class MainViewModelTest {
    @Test
    fun `greet()メソッドが "こんにちは" を返すこと`() {
        val viewModel = MainViewModel()
        Assert.assertThat(viewModel.greet(), `is`("こんにちは"))
    }
}
```
これで`ResourceFinder`の代わりに`ShadowResourceFinder`が使われるようになります。

で、結果はというと...

```text
Process finished with exit code 0
```

やりました！

`ShadowApplication`を作って`shadows`に指定してみたものの、うまくいかなかったのでこうなりました。

これでテストは走るようになったので満足です。

とりあえずは以上です。