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" />
$ 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
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 なんでしょうけど。