Unit Test for SqlDelight in Kotlin Multiplatform Mobile

이태훈·2022년 9월 5일
0

안녕하세요, 오늘은 KMM에서 로컬디비로 많이 쓰이는 SqlDelight를 Unit Testing 해보겠습니다.

우선적으로 Unit Test 환경은 인텔리제이에서 구성하겠습니다. 현재 안드로이드 스튜디오 최신 릴리즈인 Chipmunk에서 androidTest sourceSet에 대한 오류가 있어 인텔리제이로 유닛테스트 환경을 구성하면 정상적으로 작동합니다.

https://github.com/corona-warn-app/cwa-app-android/issues/5181
https://issuetracker.google.com/issues/233788419

개발 환경

  • IDE : IntelliJ IDEA 2022.2.1 (Ultimate Edition)
    Build #IU-222.3739.54, built on August 16, 2022
  • AGP : 7.2.1
  • Kotlin : 1.7.10
  • Sqldelite : 1.5.3

Gradle 작성

먼저 gradle을 설정해줍니다.

// shared/build.gradle.kts

kotlin {
  sourceSets {
    val commonTest by getting {
      dependencies {
        implementation(kotlin("test"))
      }
    }
    val androidTest by getting {
      dependencies {
        implementation("com.squareup.sqldelight:sqlite-driver:1.5.3")
      }
    }
    val iosTest by getting
  }
}

android단에서 AndroidSqliteDriver는 안드로이드 컨텍스트가 필요하니 안드로이드 종속적이지 않은 JdbcSqliteDriver를 사용하기 위해 해당 라이브러리를 주입시켜줍니다.

테스트용 SqliteDriver 생성

다음으로 각 sourceSets에 가서 공통 테스트 sqlitedriver를 만들어줍니다.

// commonTest/TestSqlDirverFactory.kt

expect class TestSqlDriverFactory() {
    fun createDriver(): SqlDriver
}

// androidTest/TestSqlDriverFactory.kt

// Foreign Key를 사용하지 않을 경우
actual class TestSqlDriverFactory {
    actual fun createDriver(): SqlDriver {
        return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
          .also(AppDatabase.Schema::create)
    }
}

// Foreign Key를 사용할 경우
actual class TestSqlDriverFactory {
    actual fun createDriver(): SqlDriver {
        val foreignProps = Properties().apply { put("foreign_keys", "true") }
        val dbName = JdbcSqliteDriver.IN_MEMORY

        return JdbcSqliteDriver(dbName, foreignProps).also(AppDatabase.Schema::create)
    }
}

// iosTest/TestSqlDriverFactory.kt

// Foreign Key를 사용할 경우
actual class TestSqlDriverFactory {
    actual fun createDriver(): SqlDriver {
        return NativeSqliteDriver(
            configuration = DatabaseConfiguration(
                name = "unit_test.db",
                version = AppDatabase.Schema.version,
                inMemory = true,
                create = { connection ->
                    wrapConnection(connection) {
                        AppDatabase.Schema.create(it)
                    }
                }
            )
        ).also {
            it.execute(null, "PRAGMA foreign_keys=ON", 0)
        }
    }
}

// Foreign Key를 사용하지 않을 경우
actual class TestSqlDriverFactory {
    actual fun createDriver(): SqlDriver {
        return NativeSqliteDriver(
            configuration = DatabaseConfiguration(
                name = "unit_test.db",
                version = AppDatabase.Schema.version,
                inMemory = true,
                create = { connection ->
                    wrapConnection(connection) {
                        AppDatabase.Schema.create(it)
                    }
                }
            )
        )
    }
}

저는 Foreign Key를 사용하고, 퍼포먼스를 높이기 위해 메모리에 데이터를 저장하는 옵션을 지정해줬습니다.

테이블 생성

다음으로 간단히 테스트 코드를 작성해주시면 됩니다.
먼저 간단한 테이블을 만들어보겠습니다.

CREATE TABLE Parent(
	name TEXT NOT NULL PRIMARY KEY
);

select:
SELECT * FROM Parent;

insert:
INSERT INTO Parent(name) VALUES ?;

delete:
DELETE FROM Parent;
CREATE TABLE Child(
	name TEXT NOT NULL PRIMARY KEY,
    parentName TEXT NOT NULL,
    FOREIGN KEY(parentName) REFERENCES Parent(parentName) ON DELETE CASCADE
);

selectAll:
SELECT * FROM Child;

insert:
INSERT INTO Child(name, parentName) VALUES ?;

이 두 테이블을 가지고 parent 테이블의 레코드가 삭제되면 cascade 정책에 따라 child 테이블의 레코드가 잘 삭제되는 지 테스트하는 코드를 작성해보겠습니다.

테스트 코드 작성

class SqlDelightTest {

    private lateinit var driver: SqlDriver
    private lateinit var database: AppDatabase

    @BeforeTest
    fun before() {
        driver = TestSqlDriverFactory().createDriver()
        database = AppDatabase(driver)
    }

    @Test
    fun `child data should be null when on delete cascade`() {
        val name = "leetaehoon"
        val parent = Parent(name)
        val child = Child("child", name)
        database.parentQueries.insert(parent)
        database.childQueries.insert(child)

        var actual = database.childQueries.selectAll().executeAsList()
        assertEquals(listOf(child), actual)

        database.parentQueries.delete()
        assertEquals(null, database.parentQueries.select().executeAsOneOrNull())

        actual = database.childQueries.selectAll().executeAsList()
        assertEquals(emptyList(), actual)
    }
}

저는 이런식으로 foreign key를 적용해서 on delete cascade를 지정했을 때 의도대로 테이블의 레코드가 잘 지워지는지 확인하는 테스트를 작성해봤습니다.

KMM에서 SqlDelight를 테스트하기 위해 중요한 점을 다시 한번 짚어보겠습니다.

  • 안드로이드 스튜디오 IDE는 불안정하니 인텔리제이에서 유닛 테스트 환경 구성 및 코드 작성하기
  • foreign key를 적용하기 위해서는 driver를 생성하고 동시에 PRAGMA foreign_keys=ON 과 같은 커맨드를 추가해줘야 한다.

이상입니다. 봐주셔서 감사합니다.
지적과 질문은 언제든지 환영입니다.


References

profile
https://www.linkedin.com/in/%ED%83%9C%ED%9B%88-%EC%9D%B4-7b9563237

0개의 댓글