일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 막내의막무가내 일상
- flutter network call
- 프로그래머스 알고리즘
- 프래그먼트
- 막내의막무가내 안드로이드 코틀린
- 주택가 잠실새내
- 주엽역 생활맥주
- 막내의막무가내 플러터
- 부스트코스
- 막내의막무가내 플러터 flutter
- 부스트코스에이스
- 안드로이드 sunflower
- 막내의 막무가내 알고리즘
- 막내의막무가내 안드로이드 에러 해결
- 막내의 막무가내
- 막내의막무가내 코볼 COBOL
- 막내의막무가내 목표 및 회고
- 막내의막무가내
- 막내의막무가내 SQL
- 막내의막무가내 알고리즘
- 막내의막무가내 rxjava
- 막무가내
- 막내의막무가내 안드로이드
- Fragment
- 막내의막무가내 프로그래밍
- 안드로이드 Sunflower 스터디
- 막내의막무가내 코틀린
- 막내의막무가내 코틀린 안드로이드
- 안드로이드
- 2022년 6월 일상
- Today
- Total
막내의 막무가내 프로그래밍 & 일상
[안드로이드] Android UnitTest 정리 본문
안드로이드 유닛테스트 기본기에 대해 정리하는 포스팅을 하려고 합니다.
안드로이드에는 계측테스트(androidTest)와 로컬단위테스트(test) 로 유닛테스트가 두 종류가 있습니다.
쉽게 설명하면 androidTest는 안드로이드 프레임워크에 종속성이 있는 테스트
test는 안드로이드 프레임워크와 관련없이 할 수 있는 테스트 들입니다. 예를들어 일반 인텔리제이에서 알고리즘 테스트 코드 짜는거 같은 것 말이죠 JVM만 있으면 되는 ㅇㅇ
이에 대한 자세한 설명은 다음을 참고하시면 됩니다 :)
이번 포스팅에서 안드로이드 유닛테스트 시 필요한 라이브러리들입니다.
testImplementation 'junit:junit:4.13'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1"
testImplementation "com.google.truth:truth:1.0.1"
testImplementation 'androidx.test.ext:junit:1.1.2'
testImplementation "org.robolectric:robolectric:4.4"
androidTestImplementation "junit:junit:4.13"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1"
androidTestImplementation "com.google.truth:truth:1.0.1"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
먼저 안드로이드 프레임워크가 필요없고 JVM에서 실행되는 테스트인로컬단위 테스트(test)부터 실습코드와 함께 살펴보겠습니다.
1. 첫번째 예제 (일반 클래스 + Junit4 + Truth)
유닛테스트를 할 MyCalc 클래스입니다.
반지름길이를 매개변수로 받아 원의 둘레와 넓이를 구해서 리턴해주는 함수들입니다.
[테스트할 클래스]
class MyCalc : Calculations {
private val pi = 3.14
override fun calculateCircumference(radius: Double): Double {
return 2 * pi * radius
}
override fun calculateArea(radius: Double): Double {
return pi * radius * radius
}
}
[테스트코드]
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
class MyCalcTest{
private lateinit var myCalc: MyCalc
@Before
fun setUp() {
myCalc = MyCalc()
}
@Test
fun calculateCircumference_radiusGiven_returnsCorrectResult(){
val result = myCalc.calculateCircumference(2.1)
assertThat(result).isEqualTo(13.188)
}
@Test
fun calculateCircumference_zeroRadius_returnsCorrectResult(){
val result = myCalc.calculateCircumference(0.0)
assertThat(result).isEqualTo(0.0)
}
@Test
fun calculateArea_radiusGiven_returnsCorrectResult(){
val result = myCalc.calculateArea(2.1)
assertThat(result).isEqualTo(13.8474)
}
@Test
fun calculateArea_zeroRadius_returnsCorrectResult(){
val result = myCalc.calculateCircumference(0.0)
assertThat(result).isEqualTo(0.0)
}
}
혹시 테스트코드 생성을 할줄 모르시는 분들은 다음사진들을 참고하시면 됩니다.
위 테스트코드 생성하는 방법 사진과 테스트 코드를(After는 없지만) 보면
@Before, @After가 있는데요. 이는 유닛테스트 클래스의 테스트 함수가 실행되기 전과 모두 끝날때 실행될 함수입니다.
안드로이드로 치면 onCreate(), onDestory()라고 이해하면 쉽겠네요 ㅎㅎ
여기서 구현한 테스트 프로세스를 간단하게 정리하면 다음과 같습니다.
1. 먼저 MyCalc() 클래스를 테스트할거니깐 @Before에 미리 클래스를 생성해주도록 합니다.
2. 그 후 해당 클래스의 함수에 원하는 함수를 호출 후 Truth 테스팅 라이브러리의 assertThat(), isEqualTo()와 같은 함수를 활용하여 내가 원하는 값이 나오는지 테스트 해줍니다. assertThat()에는 내가 테스트할려는 값을 넣어주면 됩니다.
3. 테스트 통과한 것은 초록색 실패면 빨간색 결과를 확인하게 됩니다. 끝
Truth 라이브러리에 대해 좀 더 살펴보면 다음과 같습니다.
import com.google.common.truth.Truth.assertThat
assertThat(result).isEqualTo(0.0)
androidTestImplementation "com.google.truth:truth:1.0.1"
Truth 는 Guava 팀에서 개발한 Assertion 테스팅 라이브러리중 하나입니다. 지금까지 사용했던 Junit4 대신에 사용할 수 있으며 다양한 기능과 보기 좋은 읽기쉬운 테스팅 코드를 만들 수 있습니다. (Junit4 테스트 코드 예제도 맛보기로 밑에 추가해놨습니다! ) 밑은 구글문서의 예제 및 설명입니다. 구글 예제의 경우 Truth를 import 해놓았기 때문에 Truth를 생략해서 assertThat으로 사용할 수 있습니다.
참고로 Truth의 assertThat() 매개변수에 따라 다른 테스팅 함수가 나옵니다. 그냥 추가해봤습니다 :)
Truth 말고 Junit4의 Assert로도 값 검증 테스트가 가능합니다. 다른 프로젝트에서 사용했던 것을 예제로 첨부해봤습니다. ㅎ
+) 추가로 유닛테스트에도 네이밍 컨벤션이 존재합니다. Unit Test 작성시 네이밍 컨벤션은 밑 링크를 참고하세요 위 테스트 코드 예제에서는 밑 사진의 테스트코드 네이밍 컨벤션을 사용했습니다.
medium.com/@stefanovskyi/unit-test-naming-conventions-dd9208eadbea
이상 일반 클래스의 기본적인 테스트를 살펴보았습니다. ㅎㅎ
2. 두번째 예제 (ViewModel + LiveData + Mockito + Junit4 + Truth)
두번째로 안드로이드에 자주 사용되는 ViewModel 테스팅 방법에 대해 살펴보겠습니다.
보통 ViewModel은 로직과 값을 갖고 있는 클래스이 때문에 실제 안드로이드 테스트도 가장 활발하게하고 중요합니다. ViewModel의 함수가 호출된 후 LiveData에 값이 잘 세팅 되는지 아니면 어떤 에러가 나는지 테스팅하게 됩니다.
다음은 테스트할 VIewModel의 예시코드입니다. 이전에 예제와 똑같이 원의 지름과 넓이를 계산 하고 라이브데이터에 세팅해주는 로직만 추가되었다 생각하시면 됩니다.
[테스트할 클래스]
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.lang.Exception
class CalcViewModel(
private val calculations: Calculations
) : ViewModel() {
var radius = MutableLiveData<String>()
var area = MutableLiveData<String>()
val areaValue: LiveData<String>
get() = area
var circumference = MutableLiveData<String>()
val circumferenceValue: LiveData<String>
get() = circumference
fun calculate() {
try {
val radiusDoubleValue = radius.value?.toDouble()
if (radiusDoubleValue != null) {
calculateArea(radiusDoubleValue)
calculateCircumference(radiusDoubleValue)
} else {
area.value = null
circumference.value = null
}
} catch (e: Exception) {
Log.i("MYTAG", e.message.toString())
area.value = null
circumference.value = null
}
}
fun calculateArea(radius: Double) {
val calculatedArea = calculations.calculateArea(radius)
area.value = calculatedArea.toString()
}
fun calculateCircumference(radius: Double) {
val calculatedCircumference = calculations.calculateCircumference(radius)
circumference.value = calculatedCircumference.toString()
}
}
여기서도 test 디렉토리에 유닛테스트를 작성했습니다.
[테스트코드]
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
class CalcViewModelTest{
private lateinit var calcViewModel: CalcViewModel
private lateinit var calculations: Calculations
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setUp() {
calculations = Mockito.mock(Calculations::class.java)
Mockito.`when`(calculations.calculateArea(2.1)).thenReturn(13.8474)
Mockito.`when`(calculations.calculateCircumference(1.0)).thenReturn(6.28)
calcViewModel = CalcViewModel(calculations)
}
@Test
fun calculateArea_radiusSent_updateLiveData(){
calcViewModel.calculateArea(2.1)
val result = calcViewModel.areaValue.value
assertThat(result).isEqualTo("13.8474")
}
@Test
fun calculateCircumference_radiusSent_updateLiveData(){
calcViewModel.calculateCircumference(1.0)
val result = calcViewModel.circumferenceValue.value
assertThat(result).isEqualTo("6.28")
}
}
처음 본 유닛테스트와 달라진 점을 하나씩 살펴보도록 하겠습니다.
1. 먼저 @get:Rule 입니다.
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
Rule 말 그대로 규칙을 추가하는 것인데 위에 설정한 규칙은 백그라운드 작업과 연관된 모든 아키텍처 컴포넌트들을 같은(한개의) 스레드에서 실행되게 해서 테스트 결과들이 동기적으로 실행되게 해줍니다.
즉 모든 작업들을 동기적(synchronous) 하게 해주어 동기화에 신경쓰지 않게 해주어 좋고 LiveData 테스트시 필수로 사용된다고 생각하시면 됩니다.
자세한 내용은 다음을 참고해주세요 :)
2. 두번째로 Mockito 입니다. Mockito는 Test Double(여기서 Double은 대역이란 뜻이다) 중 하나이며 Mock은 호출에 대한 기대를 명세하고, 해당 내용에 따라 동작하도록 프로그래밍 된 객체입니다.
쉽게 설명하면 원활한 테스트를 위해 필요한 객체를 Mocking해서 이 객체의 어떤 함수를 호출하면 이런 것들을 반환하게 할거다 정할 수 있다고 생각하시면 됩니다. 단 파라미터나 리턴값의 타입은 맞춰야겠죠 (내 마음대로 객체의 행동을 어떻게 할지 제어한다고 생각!!)
이때 Mockito에는 보통 구현객체가 아닌 인터페이스를 주로 사용합니다. (인터페이스를 구현한 객체를 Mockito when에 넣으면 에러나서 고생했던 적이 있네요. 인터페이스 넣어서 해결했었습니다. 원인 아시는분은 댓글 부탁드려요 ㅎㅎ )
TestDouble 에 대해서는 다음 한국 사이트에서 정리를 잘 해놓으셨습니다. 참고해주세요 :)
Mockito 코드에 대해 주석으로 설명 적어놨습니다. :)
//참고 : Mockito import 시 Mockito 는 생략가능합니다.
@Before
fun setUp() {
//내가 Mocking 할 클래스를 먼저 mock() 해준다.
calculations = Mockito.mock(Calculations::class.java)
//Mocking한 클래스를 `when`()안에 실행할 함수를 호출하고
//thenReturn()에는 내가 `when`과 똑같은 함수 호출시 반환할 값을 명시해준다.
Mockito.`when`(calculations.calculateArea(2.1)).thenReturn(13.8474)
Mockito.`when`(calculations.calculateCircumference(1.0)).thenReturn(6.28)
calcViewModel = CalcViewModel(calculations)
}
3. 세번째 예제 (ViewModel + LiveData + 비동기로 데이터 불러오는 함수 테스트 + getOrAwaitValue() + Mockito + Junit4 + Truth)
[테스트할 클래스]
뷰모델에서 비동기로 UseCase( 혹은 DataSource, Repository ) 에서 데이터를 비동기로 가져왔을 때 LiveData 값이 잘 세팅되는지에 대한 테스트를 진행할 것 입니다. 이전 예제와 다르게 비동기로 데이터를 불러와 라이브데이터에 세팅되고 이를 옵저빙 해야한다는 차이점이 생겼습니다.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.anushka.tmdbclient.domain.usecase.GetMoviesUseCase
import com.anushka.tmdbclient.domain.usecase.UpdateMoviesUsecase
class MovieViewModel(
private val getMoviesUseCase: GetMoviesUseCase,
private val updateMoviesUsecase: UpdateMoviesUsecase
) : ViewModel() {
fun getMovies() = liveData {
val movieList = getMoviesUseCase.execute()
emit(movieList)
}
fun updateMovies() = liveData {
val movieList = updateMoviesUsecase.execute()
emit(movieList)
}
}
[LiveDataTestUtil.kt 추가]
이게 이번 예제의 키포인트입니다.
비동기로 받아오는 데이터를 다음 LiveData 확장함수를 추가함으로써 테스팅이 가능하게 됩니다.
이 함수는 해당 라이브데이터가 값을 갖고있다면 즉시 반환하고 새로운 값을 받을때까지 라이브데이터를 옵저빙하고 받으면 옵저빙을 해제합니다. (기본값으로는 2초동안 기다리며 만약 2초가 지나서 값이 세팅되지 않으면 exception 을 발생시킵니다. 시간은 생성자 매개변수로 조정할 수 있습니다.)
구글에서도 이것을 사용하라고 권장하고 있다고 합니다.
자세한 사항은 다음 사이트를 참고해주세요 :)
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
@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
}
[FakeRepository]
테스트에 필요한 Data 레이어의 레포지토리를 Fake로 만들어주도록 합니다.
import com.anushka.tmdbclient.data.model.movie.Movie
import com.anushka.tmdbclient.domain.repository.MovieRepository
class FakeMovieRepository : MovieRepository {
private val movies = mutableListOf<Movie>()
init {
movies.add(Movie(1, "overview1", "path1", "date1", "title1"))
movies.add(Movie(2, "overview2", "path2", "date2", "title2"))
}
override suspend fun getMovies(): List<Movie>? {
return movies
}
override suspend fun updateMovies(): List<Movie>? {
movies.clear()
movies.add(Movie(3, "overview3", "path3", "date3", "title3"))
movies.add(Movie(4, "overview4", "path4", "date4", "title4"))
return movies
}
}
[테스트코드]
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.anushka.tmdbclient.data.model.movie.Movie
import com.anushka.tmdbclient.data.repository.movie.FakeMovieRepository
import com.anushka.tmdbclient.domain.usecase.GetMoviesUseCase
import com.anushka.tmdbclient.domain.usecase.UpdateMoviesUsecase
import com.anushka.tmdbclient.getOrAwaitValue
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MovieViewModelTest{
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var viewModel: MovieViewModel
@Before
fun setUp() {
val fakeMovieRepository = FakeMovieRepository()
val getMoviesUsecase = GetMoviesUseCase(fakeMovieRepository)
val updateMoviesUsecase = UpdateMoviesUsecase(fakeMovieRepository)
viewModel = MovieViewModel(getMoviesUsecase,updateMoviesUsecase)
}
@Test
fun getMovies_returnsCurrentList(){
val movies = mutableListOf<Movie>()
movies.add(Movie(1, "overview1", "path1", "date1", "title1"))
movies.add(Movie(2, "overview2", "path2", "date2", "title2"))
val currentList = viewModel.getMovies().getOrAwaitValue()
assertThat(currentList).isEqualTo(movies)
}
@Test
fun updateMovies_returnsUpdatedList(){
val movies = mutableListOf<Movie>()
movies.add(Movie(3, "overview3", "path3", "date3", "title3"))
movies.add(Movie(4, "overview4", "path4", "date4", "title4"))
val updatedList = viewModel.updateMovies().getOrAwaitValue()
assertThat(updatedList).isEqualTo(movies)
}
}
이전 예제와 다르게 처음 보는것 중 @RunWith(AndroidJUnit4::class) 와 Truth 가 있는데 이것에 대해서는 바로 다음 내용에서 참고할 수 있으니 뒤에서 참고 부탁드립니다. :)
지금까지 로컬단위테스트(test)를 살펴보았는데 이제 안드로이드에 종속된 계측테스트(androidTest) 예제를 보겠습니다.
1. Room Unit Test (ViewModel + LiveData + Mockito + Junit4)
Room DB는 안드로이드 Jetpack Components 중 하나로 안드로이드 종속성이 필요하기 때문에 Room 유닛테스트를 위해서는 AnroidTest 디렉토리에 테스트코드를 만들어야합니다.
Room DB의 DAO 클래스의 쿼리가 제대로 동작하는지에 대한 테스트를 해보겠습니다.
[테스트할 Room DAO 클래스]
@Dao
interface MovieDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveMovies(movies : List<Movie>)
@Query("DELETE FROM popular_movies")
suspend fun deleteAllMovies()
@Query("SELECT * FROM popular_movies")
suspend fun getMovies():List<Movie>
}
[Room Unit Test]
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Database
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.anushka.tmdbclient.data.model.movie.Movie
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(AndroidJUnit4::class)
class MovieDaoTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var dao: MovieDao
private lateinit var database: TMDBDatabase
@Before
fun setUp() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
TMDBDatabase::class.java
).build()
dao = database.movieDao()
}
@After
fun tearDown() {
database.close()
}
@Test
fun saveMoviesTest() = runBlocking{
val movies = listOf(
Movie(1,"overview1","posterPath1","date1","title1"),
Movie(2,"overview2","posterPath2","date2","title2"),
Movie(3,"overview3","posterPath3","date3","title3")
)
dao.saveMovies(movies)
val allMovies = dao.getMovies()
Truth.assertThat(allMovies).isEqualTo(movies)
}
@Test
fun deleteMoviesTest() = runBlocking {
val movies = listOf(
Movie(1,"overview1","posterPath1","date1","title1"),
Movie(2,"overview2","posterPath2","date2","title2"),
Movie(3,"overview3","posterPath3","date3","title3")
)
dao.saveMovies(movies)
dao.deleteAllMovies()
val moviesResult = dao.getMovies()
Truth.assertThat(moviesResult).isEmpty()
}
}
Room 테스트 코드 프로세스는 다음과 같습니다.
@Before 에서 Room 데이터베이스와 DAO를 미리 만들어줍니다.
그 후 Room DAO 메소드들을 테스트한 후 @After 에서 데이터베이스 커넥션을 close() 해줍니다.
이전에 다룬거 외에 새로본것들을 하나씩 살펴보겠습니다. 룸이 무엇인지 그런거에 대한건 주제를 벗어나므로 다루지 않겠습니다.
1. @RunWith(AndroidJUnit4::class)
@RunWith(AndroidJUnit4::class)
class MovieDaoTest {
개념은 역시 문서를 보는게 최고입니다. 밑을 참고해주세요
2. runBlocking
runBlocking() 함수는 어떠한 코드 블록 내 작업이 완료 되기를 기다리는 가장 간단한 방법 중 하나이며 룸 테스트코드들이 순차적으로 진행될 수 있도록 해주기 위해 추가합니다.
이외에 Koin 과 같은 DI를 사용한 Room DAO Unit Test는 다음 링크를 참고하면 될 것 같습니다.
기타 화면 UI 테스트하는데 사용하는 Espresso와 RxJava2 테스팅은 이전에 썼던 블로그 포스팅 링크를 밑에 첨부해놨습니다.
이상 기본적인 유닛테스트에 대해 모두 살펴보았습니다. ㅎㅎ
기타 화면 UI 테스트하는데 사용하는 Espresso와 RxJava2 테스팅은 이전에 썼던 블로그 포스팅을 첨부하겠습니다. :)
[Espresso를 사용한 UI 유닛테스트]
youngest-programming.tistory.com/370?category=982808
[RxJava2 유닛테스트 - 링크 하단 부 ]
youngest-programming.tistory.com/360?category=982808
[참고]
developer.android.com/studio/test?hl=ko#test_types_and_location
[티스토리는 글자크기 조정을 어케하는 것인가 쓸때 글자크기와 작성 후 글자크기가 다르고 뒤죽박죽에 이상하다... 제목1, 본문1, 본문2 나눠서 작성했는데 ..]
댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!
'안드로이드 > Unit Test' 카테고리의 다른 글
[코틀린] 안드로이드 UI Unit Test with Espresso (2) | 2020.09.04 |
---|---|
[안드로이드] Android Unit Test Coverage with Jacoco 안드로이드 통합 테스트 하는 방법 (Kotlin) (0) | 2020.08.23 |
[안드로이드] ViewModel, LiveData, RxJava UnitTest (0) | 2020.08.16 |