#1 ์ด์ ๊ธ
#1-1 Unit Testing ๊ฐ์
[Android] Unit Testing - ๊ฐ์์ ํ๊ฒฝ ์ค์
#1 ์๋๋ก์ด๋ ์ฑ ํ ์คํธ#1-1 ์๋๋ก์ด๋ ์ฑ ํ ์คํธ์ ์ข ๋ฅ๋จผ์ , ์ฌ๊ธฐ์ ์๋ ๊ตฌ๊ธ ๊ณต์ ๋ฌธ์์์ ์๋๋ก์ด๋ ์ฑ ํ ์คํธ์ ๋ํ ๊ฐ์๋ฅผ ์ฝ์ผ๋ฉด ์ข๋ค. ํด๋น ๊ตฌ๊ธ ๊ณต์ ๋ฌธ์์์ ๋ณต์ฌํด์จ ์์ ๊ทธ๋ฆผ
kenel.tistory.com
์ ๋งํฌ์ ์๋ ์ด์ ๊ฒ์๊ธ์ ์ด์ด์, ์ค์ ์๋๋ก์ด๋ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด Room ๋ฐ LiveData์ Unit Testing์ ์ํํด๋ณธ๋ค. ๋ณธ ๊ฒ์๊ธ์ ์ฝ๊ธฐ ์ ์ [Android] Unit Testing - ๊ธฐ์ด๋ฅผ ๋ณด๊ณ ์ค๋ฉด ์ดํด์ ๋์์ด ๋๋ค.
#1-2 ํ๊ฒฝ ์ค์ (build.gradle ๋ฑ)
์ด์ ๊ฒ์๊ธ์ #3์ ํ ๋๋ก ๋ณธ ๊ฒ์๊ธ์ ๋์ค๋ ์๋๋ก์ด๋ ํ๋ก์ ํธ์ Gradle, AGP, JDK์ ๋ฒ์ ์ค์ ๋ฐ build.gradle ์ค์ ์ ์งํํ๋ค. ์ด์ ๊ฒ์๊ธ์ build.gradle๊ณผ ๋ฌ๋ฆฌ, ๋ณธ ๊ฒ์๊ธ์์๋ Unit Testing์ ์ฌ์ฉ๋์ง ์๋ build.gradle์ plugins { ... }, buildFeatures { ... }, dependencies { ... }์ ์ผ๋ถ ์์๋ฅผ ์ ๊ฑฐํ๋ค. ์ด๋ ์ฝ๋ ๋ค์ด์ดํธ๋ฅผ ์ํ ๊ฐ์ธ์ ์ธ ์ ๊ฑฐ์ด๊ธฐ ๋๋ฌธ์, ์ด ๊ธ์ ๋ณด๋ ์ฌ๋์ ์ ๊ฑฐ ์์ด ๊ทธ๋ฅ ๋ณต์ฌ ๋ฐ ๋ถ์ฌ๋ฃ๊ธฐํด๋ ๋๋ค.
#2 Unit Testing์ ์ ์ฉํ ์ํ ์ฑ
[Android] Room - ๊ธฐ์ด, INSERT์ DELETE ์ฐ์ต
#1 Room ์๊ฐ Room์ ์ฌ์ฉํ์ฌ ๋ก์ปฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ ์ ์ฅ | Android Developers Room ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ฝ๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ์งํ๋ ๋ฐฉ๋ฒ ์์๋ณด๊ธฐ developer.android.com SQLite๋ ๋ชจ๋ฐ์ผ ๊ธฐ๊ธฐ๋ฅผ ์ํ
kenel.tistory.com
์ด ๊ฒ์๊ธ์ ์์ฑ๋ ์ฑ์ ๊ฐ์ ธ์์ ํ ์คํธ๋ฅผ ์ํํ๋ค. ํด๋น ์ฑ์ ๊ฐ์ ธ์์ #1-2์ ๋ง๊ฒ ํ๊ฒฝ ์ค์ ์ ์์ ํ๋ค. ์ ์ฒด ์์ค ์ฝ๋๋ ๋ณธ ๊ฒ์๊ธ์ #5์ ์๋ค.
#3 Unit Testing
#3-1 ํ ์คํธ ํด๋์ค ์์ฑ

๋ด๊ฐ ํ ์คํธํ ํด๋์ค๋ UserDAO๋ค. ์ด๋ ์ธํฐํ์ด์ค์ด๊ธฐ์ ์ด ๊ฒ์๊ธ์ #3-1์์ ์งํํ ๊ฒ์ฒ๋ผ ํ ์คํธ ํด๋์ค ์์ฑ์ ํ ์๋ ์๋ค ([Generate...] ๋ฉ๋ด ๋ค์์ผ๋ก [Test...] ๋ฉ๋ด๊ฐ ์ ๋ธ). ํ์ง๋ง, ๊ทธ๋ฅ ์ง์ ์๋์ผ๋ก ํ ์คํธ ํด๋์ค๋ฅผ ๋ง๋ค๋ฉด ๊ทธ๋ง์ด๋ค. Room ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํด์ผํ๊ธฐ ๋๋ฌธ์ Instrumented test๋ก ์งํํ๋ค. ๋ฐ๋ผ์, androidTest ๋๋ ํ ๋ฆฌ์ ํ ์คํธ ํด๋์ค๋ฅผ ์์ฑํ๋ค. LiveDataTestUitl.kt๋ผ๋ ํ์ผ๋ ๋ณด์ด๋๋ฐ, ๊ณง๋ฐ๋ก ์ค๋ช ํ์๋ฉด
#3-2 LiveDataTestUitl
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
// ์ฝ๋ ์ถ์ฒ: https://stackoverflow.com/questions/73292145/android-unit-testing-kotlin-livedata-value-was-never-set
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
์ฌ๊ธฐ์ ์๋ ์์ค ์ฝ๋๋ฅผ ๊ฐ์ ธ์๋ค. ์ด ํด๋์ค๋ LiveData๋ฅผ ํ ์คํธํ ๋ ์ ์ฉํ๋ค. ๋ณธ ๊ฒ์๊ธ์์ ์ฌ์ฉํ ์ํ ์ฑ์ Room์ ๋ฐํ๊ฐ์ผ๋ก LiveData๋ฅผ ๋ด๋ฑ๋๋ค. ์ด ๋, LiveData์ value๋ฅผ ์ฐธ์กฐํ๋ ค๋ฉด, observe()๋ฅผ ํตํด, LiveData์ ๊ฐ์ด ๋ณํํ๋ ์๊ฐ์ ํฌ์ฐฉํด์ผํ๋ค. ํ์ง๋ง, ํ ์คํธ ํด๋์ค์์ observe() ๋ฑ์ ๊ตฌํํ๋ ๊ฒ์ ๊ท์ฐฎ์ ์ผ์ด๋ค. LiveData.observe()์ ๋์์ Lifecycle์ด ๊ด์ฌํ๊ธฐ ๋๋ฌธ์ด๋ค. LiveDataTestUitl ํด๋์ค์ getOrAwaitValue()๋ LiveData์ value๋ฅผ ์ง๊ด์ ์ผ๋ก ๊ฐ์ ธ์ค๊ฒ ๋ง๋ค์ด์ค๋ค.
#3-3 ํ ์คํธ ํด๋์ค์ ๋ด์ฉ ์์ฑ
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.testingroom.getOrAwaitValue
import com.google.common.truth.Truth
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/* @RunWith ์ด๋
ธํ
์ด์
์ด ์์ผ๋ฉด,
* JUnit์ด ๊ธฐ๋ณธ ํ
์คํธ ๋ฌ๋(Test Runner)์ธ JUnit4 ํ
์คํธ ๋ฌ๋๋ฅผ ์ฌ์ฉํ๋ค.
* AndroidJUnit4 ํ
์คํธ ๋ฌ๋์๋ ๋ฌ๋ฆฌ, JUnit4 ํ
์คํธ ๋ฌ๋๋ ์๋๋ก์ด๋ ํ๊ฒฝ์ ๋ง์ถฐ์ ธ ์์ง ์์ผ๋ฏ๋ก,
* ์๋ฌ์ ์ฌ์ง๊ฐ ์๊ฒจ๋ฒ๋ฆฐ๋ค.
*/
@RunWith(AndroidJUnit4::class)
class UserDAOTest {
/* InstantTaskExecutorRule ๊ท์น(Rule)
* ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ ์คํ๋๋ LiveData๋ฅผ ๋ฉ์ธ ์ค๋ ๋์์ ์คํ๋๋๋ก ๊ฐ์ ํจ.
* LiveData๊ฐ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ ๊ทธ ๊ฐ์ด ๋ณ๊ฒฝ๋๋ค๋ฉด, ๋ฉ์ธ ์ค๋ ๋์์ ๋ณํ๋ฅผ ๊ฐ์งํ์ง ๋ชปํ ์๋ ์๊ธฐ ๋๋ฌธ.
* ๋, ๋ฐ์ดํฐ์ ๋ณํ๋ฅผ ๊ฐ์งํ๊ธฐ ์ ์ ํ
์คํธ๊ฐ ๋๋๋ฒ๋ฆด ์ฌ์ง๋ ์์.
* ๋ฐ๋ผ์, LiveData๊ฐ ๊ด์ฌ๋๋ ํ
์คํธ์๋ InstantTaskExecutorRule ๊ท์น์ด ๋ง์ด ์ฌ์ฉ๋จ.
*/
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var dao: UserDAO
private lateinit var database: UserDatabase
@Before
fun setUp() {
// Dependency ์ด๊ธฐํ
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
UserDatabase::class.java
).build()
// Dependent ์ด๊ธฐํ (์ธํฐํ์ด์ค ๊ตฌํ์ฒด๋ฅผ ์ธํฐํ์ด์ค ํ๋กํผํฐ์ ๋ฃ๋ ๊ฒ๋ '์์กด์ฑ ์ฃผ์
'์ ํ ๊ฐ๋๋ค)
dao = database.userDAO
}
@After
fun tearDown() {
database.close()
}
@Test
fun insertUsersTest() {
runBlocking {
// Insert
val usersToInsert = listOf(
User(1, "steve", "helloWorld@example.com"),
User(2, "bob", "pizza1234@example.com"),
User(3, "paul", "kenel@example.com"),
User(4, "kevin", "chickenMania@example.com")
)
dao.insertUser2(usersToInsert)
/* Get
* ์๋ LiveData์ value๋ฅผ ์ฐธ์กฐํ๋ ค๋ฉด, observe()๋ฅผ ํตํด,
* LiveData์ ๊ฐ์ด ๋ณํํ๋ ์๊ฐ์ ํฌ์ฐฉํด์ผํ๋ค.
* ๊ทธ ๊ณผ์ ์ LiveDataTestUtil์์ ์์์ ์งํํด์ค๋ค.
* ๋ง์ฝ LiveDataTestUtil์ ํจ์ ์์ด,
* val usersFetched = dao.getAllUsers().value์ ๊ฐ์ ์์ ์ฝ๋์๋ค๋ฉด,
* usersFetched์๋ null์ด ๋ด๊ธฐ๊ฒ ๋๋ค.
*/
val usersFetched = dao.getAllUsers().getOrAwaitValue()
// ๋น๊ต
Truth.assertThat(usersFetched).isEqualTo(/* expected = */ usersToInsert)
}
}
@Test
fun deleteUsersTest() {
runBlocking {
// Insert
val usersToInsert = listOf(
User(1, "steve", "helloWorld@example.com"),
User(2, "bob", "pizza1234@example.com"),
User(3, "paul", "kenel@example.com"),
User(4, "kevin", "chickenMania@example.com")
)
dao.insertUser2(usersToInsert)
// Delete
dao.deleteAll()
// Get์ ํตํ ํ์ธ
val usersFetched = dao.getAllUsers().getOrAwaitValue()
Truth.assertThat(usersFetched).isEmpty()
}
}
}
Room.inMemoryDatabaseBuilder()๋ 1ํ์ฉ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ญ์ ์์ฑํ๋ค. ํ ์คํธ ์ข ๋ฃ ์์ ํด๋น ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ ์ ๊ฑฐ๋๋ฏ๋ก ๋ณธ๋์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ต๋์ ์ผ์ผํฌ ์ฌ์ง๊ฐ ์๋ค.
์ฌ๋ด์ผ๋ก Room.inMemoryDatabaseBuilder๋ฅผ ์ฌ์ฉํ์ง ์๊ณ , UserRepository๋ฅผ ์์๋ฐ๋ StubUserRepository๋ฅผ ๋ง๋๋ ๋ฐฉ์๋ ์ฌ์ฉํ ์ ์๋ค. ์ด ๊ฒฝ์ฐ๋ Room์ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ Instrumented test๊ฐ ์๋๋ผ Local unit test๋ก ์งํ์ด ๊ฐ๋ฅํ๋ค.
#4 ์๋ ํ์ธ

#5 ์์ฑ๋ ์ฑ
android-practice/unit-test/TestingRoomAndLiveData at master ยท Kanmanemone/android-practice
Contribute to Kanmanemone/android-practice development by creating an account on GitHub.
github.com
'๊นจ์ ๊ฐ๋ ๐ > Android' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Android] Data Binding - View Binding (1) | 2024.07.10 |
---|---|
[Android] Unit Testing - Retrofit (0) | 2024.07.08 |
[Android] Unit Testing - ViewModel (0) | 2024.07.08 |
[Android] Unit Testing - ๊ธฐ์ด (0) | 2024.07.08 |
[Android] Unit Testing - ๊ฐ์์ ํ๊ฒฝ ์ค์ (2) | 2024.07.03 |