Home About Contact
Android , SQLite , Robolectric

Robolectric で Android の SQLiteOpenHelper のテストを書く

Android Studio を起動するのが億劫なので、 コマンドラインだけで開発できる環境をつくろうとしている。 そこで避けて通れないのがテスト環境。

Robolectric で SQLiteOpenHelper のテスト方法 を書き残します。

環境

$ gradle --version

------------------------------------------------------------
Gradle 8.5
------------------------------------------------------------

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.9 (Private Build 17.0.9+9-Ubuntu-122.04)
OS:           Linux 5.15.0-91-generic amd64

ルートプロジェクトの準備

$ mkdir android-sqlite-test
$ cd android-sqlite-test
$ touch build.gradle
$ touch gradle.properties 
$ touch settings.gradle

build.gradle の内容を記述する。

plugins {
    id 'com.android.library' version '8.1.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
}

gradle.properties の内容を記述する。

android.useAndroidX=true

settings.gradle の内容を記述する。

pluginManagement {
  repositories {
    gradlePluginPortal()
    google()
    mavenCentral()
  }
}
dependencyResolutionManagement {
  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
  repositories {
    google()
    mavenCentral()
    mavenLocal()
  }
}

include ':data'

ライブラリプロジェクトを準備

$ mkdir data
$ cd data
$ touch build.gradle

build.gradle を編集。

plugins {
  id 'com.android.library'
  id 'org.jetbrains.kotlin.android'
}

android {
  namespace 'com.example.data'
  compileSdk 33

  defaultConfig {
    minSdk 24
    targetSdk 33
  }

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  kotlinOptions {
    jvmTarget = '1.8'
  }

  testOptions {
    unitTests {
      includeAndroidResources = true
    }
  }
}

dependencies {
  testImplementation 'junit:junit:4.13.2'
  testImplementation 'org.robolectric:robolectric:4.11.1'
}

さらに、AndroidManifest.xml を用意。

$ mkdir src/main -p
$ touch src/main/AndroidManifest.xml

AndroidManifest.xml の内容を記述。

<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

テスト対象となる SQLiteOpenHelper を継承したデータベースを用意

$ mkdir src/main/java/com/example/data -p
$ touch src/main/java/com/example/data/MyData.kt

src/main/java/com/example/data/MyData.kt の内容を記述します。

package com.example.data

import android.content.Context
import android.database.sqlite.SQLiteOpenHelper
import android.database.sqlite.SQLiteDatabase
import android.provider.BaseColumns._ID

val DBNAME = "mydata.db"
val DBV = 1

class MyData(ctx: Context): SQLiteOpenHelper(ctx, DBNAME, null, DBV){
  override fun onCreate(db: SQLiteDatabase?) {
    // TODO
  }
  override fun onUpgrade(db: SQLiteDatabase?, oldV: Int, newV: Int) {
    // TODO
  }
}

const val DBNAME とかにした方がいいのかもしれませんが、 その辺は省きます。

onCreate の実装。

users テーブルをつくり _ID フィールドと name フィールドを用意。

  override fun onCreate(db: SQLiteDatabase?) {
    val sb = StringBuilder()
    sb.append("CREATE TABLE users (")
    sb.append("${_ID} INTEGER PRIMARY KEY AUTOINCREMENT, ")
    sb.append("name TEXT NOT NULL")
    sb.append(");")
    db?.execSQL(sb.toString())
  }

onUpgrade の実装。

本来であれば、どのバージョン(oldV)からどのバージョン(newV) へアップグレードするかによってそれぞれ必要な処理を 事細かに書くことになりますが、ここでは、単純に既存の users テーブルを削除するだけにして、それから onCreate を呼ぶことで、 新規にテーブルをつくり直しています。

  override fun onUpgrade(db: SQLiteDatabase?, oldV: Int, newV: Int) {
    db?.execSQL("DROP TABLE IF EXISTS users;")
    onCreate(db)
  }

この段階で、 プロジェクトのルートに戻って、gradle assemble します。

$ gradle assemble

うまく作動したら、 gradle wrapper しておきます。

$ gradle wrapper

この段階でのプロジェクト全体像です。

.
├── build.gradle
├── data
│   ├── build.gradle
│   └── src
│       └── main
│           ├── AndroidManifest.xml
│           └── java
│               └── com
│                   └── example
│                       └── data
│                           └── MyData.kt
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

MyData にデータの追加と取得メソッドを追加

users テーブルを用意したので、User データクラスをつくりましょう。

今のカレントディレクトリがプロジェクトルートだとして・・・

$ touch data/src/main/java/com/example/data/User.kt

内容はこれです。

package com.example.data

data class User(val name: String)

それでは addUser , getUsers の2つの関数を MyData.kt に用意します。

コルーチンから使うつもりなので suspend をつけています。

addUser 関数。

  @WorkerThread
  suspend fun addUser(user: User){
    withContext(Dispatchers.IO){
      val values = ContentValues()
      values.put("name", user.name)
      writableDatabase.insert("users", null, values)
    }
  }

getUsers 関数。

  @WorkerThread
  suspend fun getUsers(): List<User>{
    return withContext(Dispatchers.IO){
      val userList = mutableListOf<User>()

      readableDatabase.query("users", arrayOf("name"), null, null, null,null, null).use { cursor ->
        val nameIndex = cursor.getColumnIndex("name")

        if (cursor.moveToFirst()) {
          do {
            val name   = cursor.getString(nameIndex)
            userList.add(User(name))
          } while (cursor.moveToNext())
        }
      }

      userList
    }
  }

ContentValues, WorkerThread, Dispatchers, withContext の import を忘れずに。

import android.content.ContentValues

import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

さらに、 data/build.gradle の依存も追加が必要です。

dependencies {
  implementation 'androidx.core:core-ktx:1.9.0'
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
  implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
  ...

assemble タスクを実行して問題がないことを確認しましょう。

$ ./gradlew assemble

テストを書く

まず簡単な UserTest から書きます。

UserTest.kt を用意してテストを書きます。

$ mkdir src/test/java/com/example/data -p
$ touch src/test/java/com/example/data/UserTest.kt

UserTest.kt の内容。

package com.example.data

import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class UserTest {
  @Test
  fun test1(){
    val user = User("foo")
    Assert.assertEquals("foo", user.name)
  }
}

test タスクを実行して問題がないことを確認しましょう。

$ ./gradlew test

いよいよ本題の MyDataTest を書きます。

$ touch src/test/java/com/example/data/MyDataTest.kt

MyDataTest.kt の内容。

@RunWith(RobolectricTestRunner::class)
class MyDataTest {
  private var myData: MyData? = null

  @Before
  fun setup() {
    // TODO データベースの生成。
  }

  @After
  fun teardown() {
    // TODO データベースの後始末。
  }

  @Test
  fun test1(){
    // TODO テストを書く。
  }
}

UserTest と異なり、setup, teardown 関数を用意します。 ここでデータベースの生成と後始末を行うためです。

テストを一つしか書くつもりがなければ、そのテストの中で済ませれば良い話ではあります。

  private var myData: MyData? = null

  @Before
  fun setup() {
    val context: Context  = ApplicationProvider.getApplicationContext()
    myData = MyData(context)
  }

  @After
  fun teardown() {
    myData?.close()
  }

新たに Context, ApplicationProvider を使うことになったので、 MyDataTest.kt の先頭で import します。

import android.content.Context
import androidx.test.core.app.ApplicationProvider

そして data/build.gradle にも依存を追加。

dependencies {
  ...
  testImplementation 'androidx.test:core-ktx:1.5.0'
}

テスト test1 の内容はユーザーを追加して取り出して件数をテストすることにします。

うまくいかないことは目に見えていますが、直接 addUser を使ってみます。

  @Test
  fun test1(){
    val user0 = User("foo")
    myData?.addUser(user0)
  }

プロジェクトのルートで ./gradlew test すると、やはりエラーが出ました。 suspend 関数なので、コルーチンからか他の suspend 関数からしか呼び出しできません、とのこと。

Suspend function 'addUser' should be called only from a coroutine or another suspend function

この問題を解決するには、コルーチンをつくってそこから該当の suspend 関数を呼び出します。 このように。

    runBlocking {
      launch {
        myData?.let {
          it.addUser(user0)
          it.addUser(user1)
        }
      }
    }

新たに runBlocking, launch を使うことになったのでそれらを import します。

import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

テスト関数全体では次のようになりました。

  @Test
  fun test1(){
    val user0 = User("foo")
    val user1 = User("bar")

    // add 2 users.
    runBlocking {
      launch {
        myData?.let {
          it.addUser(user0)
          it.addUser(user1)
        }
      }
    }

    // get all users.
    var userList: List<User> = listOf()

    runBlocking {
      launch {
        myData?.let {
          userList = it.getUsers()
        }
      }
    }

    Assert.assertEquals(2, userList.size)
  }

あとは、プロジェクトルートで ./gradlew test して、テストが成功することを確認しましょう。

まとめ

これでデータベース回り(SQLiteOpenHelper の場合)をコマンドライン+ gradle で開発とテストができるようになりました。 今どきはAndroidのデータベース操作と言えば Room なんでしょうけど。