일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 플러터 설치 2022
- rxjava cold observable
- android retrofit login
- 스택 자바 코드
- rxjava hot observable
- 큐 자바 코드
- 안드로이드 라이선스 종류
- 멤버변수
- jvm 작동 원리
- 2022 플러터 설치
- 안드로이드 유닛 테스트 예시
- 안드로이드 라이선스
- 서비스 vs 쓰레드
- 안드로이드 유닛 테스트
- 스택 큐 차이
- 안드로이드 레트로핏 사용법
- 서비스 쓰레드 차이
- 2022 플러터 안드로이드 스튜디오
- ANR이란
- rxjava disposable
- Rxjava Observable
- 객체
- 안드로이드 os 구조
- android ar 개발
- ar vr 차이
- 클래스
- 안드로이드 레트로핏 crud
- 안드로이드 유닛테스트란
- jvm이란
- 자바 다형성
- Today
- Total
나만을 위한 블로그
[Android] JUnit4 환경에서 단위 테스트 작성하는 법 (Github API) 본문
Github API로 앱이 동작하는 건 지난 포스팅에서 확인했으니, 이제 테스트를 작성해서 내가 짠 코드들이 정말 정상적으로 작동하는지 확인할 차례다. 테스트를 먼저 작성한 후 기능을 구현하는 TDD라는 개념도 있지만, 여기선 선 기능구현 후 테스트 작성이라는 상황이라고 가정한다. 이 글의 바탕이 되는 코드는 아래 링크에서 확인할 수 있다.
https://onlyfor-me-blog.tistory.com/725
깃허브 API를 쓰기 위해 필요한 토큰을 프로젝트에 세팅하는 과정은 생략한다. 아래는 인터페이스와 repository, 뷰모델의 코드다.
interface GithubApiService {
@GET("search/repositories?sort=stars")
suspend fun searchRepos(
@Query(QUERY) query: String,
@Query(PAGE) page: Int,
@Query(PER_PAGE) perPage: Int
): RepoSearchResponse
}
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asFlow
import androidx.lifecycle.asLiveData
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GithubRepository @Inject constructor(
private val service: GithubApiService
) {
fun getSearchRepoResult(query: String): LiveData<PagingData<Repo>> {
if (query.isBlank()) {
return MutableLiveData<PagingData<Repo>>(PagingData.empty()).asFlow().asLiveData()
}
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false,
prefetchDistance = 200
),
pagingSourceFactory = { GithubPagingSource(service, query) }
).flow.asLiveData()
}
}
import androidx.lifecycle.*
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class GithubViewModel @Inject constructor(
private val repository: GithubRepository
): ViewModel() {
private val _searchQuery = MutableLiveData<String>()
val searchQuery: LiveData<String> = _searchQuery
private val repoResult: LiveData<PagingData<Repo>> = _searchQuery.switchMap { query ->
if (query.isBlank()) {
MutableLiveData(PagingData.empty())
} else {
repository.getSearchRepoResult(query).cachedIn(viewModelScope)
}
}
val repos: LiveData<PagingData<Repo>> = repoResult
fun searchRepos(query: String) {
_searchQuery.value = query
}
}
이제 앱 gradle에 테스트에 필요한 의존성들을 추가해준다.
testImplementation "org.mockito:mockito-core:3.12.4"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
testImplementation "org.robolectric:robolectric:4.3.1"
testImplementation "androidx.arch.core:core-testing:2.0.0"
testImplementation "androidx.test:core-ktx:1.5.0"
testImplementation "io.mockk:mockk:1.13.3"
testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test.ext:junit-ktx:1.1.5"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.test:core-ktx:1.5.0"
androidTestImplementation "androidx.test:rules:1.5.0"
testImplementation "org.hamcrest:hamcrest:2.2"
중구난방으로 의존성들이 뒤섞여 있는데 크게 보면 아래 의존성들이 필요하다.
- mockk : 단위 테스트 작성 시 사용할 클래스들을 mocking하기 위해 필요한 라이브러리. mockito와 비슷하다.
- hamcrest : 필요한 경우 Matcher를 써서 assertion 안에서 데이터의 일치성 여부를 검사하는 등의 상황이 발생할 수 있는데, 그 때 사용할 수 있는 프레임워크. 좀 더 직관적인 Matcher를 제공함
- robolectric : 테스트 케이스는 하나만 작성하지 않는다. 하나의 기능에 대해 몇 개의 테스트를 작성할 수 있는데, 각 테스트마다 필요한 안드로이드 환경이 다른 경우가 발생할 수 있다. 이 때 테스트들이 각각의 다른 안드로이드 환경에서 실행되게 도와주는 라이브러리
이제 테스트를 작성해 본다. java 패키지는 총 3가지 종류의 패키지를 갖고 있다.
- com.example.xxx
- com.example.xxx (androidTest)
- com.example.xxx (test)
첫 번째는 앱 구현에 필요한 클래스들을 모아둔 패키지고 androidTest는 UI 테스트와 통합 테스트, test는 단위 테스트를 작성하는 파일들을 모아놓는 패키지다.
이번에 작성하는 건 단위 테스트기 때문에 (test)라고 써진 패키지에 새 클래스를 만든다.
class MyViewModelTest {
}
그리고 아래와 같이 클래스명 위에 어노테이션을 하나 추가해준다. import도 빼먹지 말고 해준다.
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class MyViewModelTest {
}
별도로 프로젝트 환경설정을 바꾸지 않았다면 프로젝트는 JUnit4로 설정돼 있을 것이다. 하지만 MyViewModelTest 클래스가 어떤 환경에서 테스트되는지는 어노테이션이 붙기 전까진 컴파일러가 알 수 없으니, 해당 테스트를 JUnit4 환경에서 구동하겠다는 뜻으로 @RunWith 어노테이션을 쓰고 JUnit4를 명시해준다.
이제 테스트를 작성해야 할까 생각되겠지만 아니다. 아직 테스트할 코드의 환경설정이 끝나지 않았다.
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.example.kotlinprac.paging.prac.GithubRepository
import com.example.kotlinprac.paging.prac.GithubViewModel
import org.junit.Rule
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class MyViewModelTest {
@get:Rule
var instantExecutorRule: TestRule = InstantTaskExecutorRule()
private lateinit var repository: GithubRepository
private lateinit var viewModel: GithubViewModel
}
InstantTaskExecutorRule()은 꼭 필요하다. 왜냐면 뷰모델의 코드를 다시 확인해 보면 LiveData를 사용하고 있다. 그리고 LiveData.postValue()는 백그라운드에서 작동한다.
테스트를 실행하면 테스트 실행기는 postValue()가 백그라운드에서 실행 중인 걸 알 수가 없다. 그래서 나중에 테스트 케이스 안에서 뷰모델의 함수를 호출할 경우 NullPointerException이 발생하면서 테스트가 깨지거나 LiveData의 값이 변경되지 않아 assertion과 다르다는 에러를 뿜으며 테스트가 실패할 수 있다. 그래서 InstantTastkExecutorRule()은 무조건 써 줘야 한다.
다음으로 내가 작성할 테스트 케이스(=@Test 어노테이션이 붙은 함수들)들이 각각 실행되기 전, 후에 수행할 코드들을 @Before, @After 어노테이션이 붙은 함수에 작성해준다.
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.example.kotlinprac.paging.prac.GithubRepository
import com.example.kotlinprac.paging.prac.GithubViewModel
import io.mockk.mockk
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class MyViewModelTest {
@get:Rule
var instantExecutorRule: TestRule = InstantTaskExecutorRule()
private lateinit var repository: GithubRepository
private lateinit var viewModel: GithubViewModel
@Before
fun setUp() {
repository = mockk(relaxed = true)
viewModel = GithubViewModel(repository)
}
@After
fun tearDown() { /* 각 테스트 종료 후 수행해야 하는 코드 작성 */ }
}
테스트 케이스마다 repository, 뷰모델을 초기화하는 코드를 넣으면 테스트 케이스들이 쓸데없이 길어져서 가독성에 좋지 않다. 그러므로 @Before를 붙인 함수를 만들고, 이 함수 안에서 각 테스트 케이스가 실행되기 전에 수행돼야 하는 코드들을 넣는다. 아래 경우들을 생각할 수 있다.
- MockWebServer 인스턴스 초기화
- 필요할 경우 테스트 케이스의 코루틴이 실행될 쓰레드를 테스트 환경에서 사용하는 쓰레드로 변경
@After가 붙은 함수 안에는 테스트가 끝난 후 실행돼야 하는 코드들을 넣는다. 아래 경우들을 생각할 수 있다.
- MockWebServer를 사용한 특정 테스트가 끝나면 해당 MockWebServer 인스턴스를 닫아야 한다
- @Before에서 코루틴이 실행되는 쓰레드를 바꾼 경우 해당 테스트가 끝나면 되돌려놔야 한다
이외에도 여러 경우가 있지만 이 포스팅에선 @After에 작성해야 하는 코드가 없으니 무시한다.
여기까지 끝났다면 드디어 테스트를 작성할 수 있다. 먼저 특정 검색어를 입력한 경우 뷰모델의 LiveData에 해당 검색어가 들어가는지 확인하는 테스트 케이스를 아래와 같이 작성한다.
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.example.kotlinprac.paging.prac.GithubRepository
import com.example.kotlinprac.paging.prac.GithubViewModel
import io.mockk.mockk
import org.junit.*
import org.junit.Assert.assertEquals
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class MyViewModelTest {
@get:Rule
var instantExecutorRule: TestRule = InstantTaskExecutorRule()
private lateinit var repository: GithubRepository
private lateinit var viewModel: GithubViewModel
@Before
fun setUp() {
repository = mockk(relaxed = true)
viewModel = GithubViewModel(repository)
}
@Test
fun `특정 검색어를 입력하고 검색한 경우 LiveData의 값이 바뀐다`() {
// Given
val query = "android"
// When
viewModel.searchRepos(query)
// Then
assertEquals(viewModel.searchQuery.value, query)
}
}
테스트 이름은 백틱을 사용하면 한글로 작성할 수 있으니 참고한다. 이렇게 작성하고 테스트를 실행하면 테스트는 성공할 것이다. Run 탭은 아래처럼 표시될 것이다.
이제 다른 테스트 케이스들을 추가해서 프로젝트를 더 견고하게 만들 수 있다.
'Android' 카테고리의 다른 글
[Android Compose] LaunchedEffect란? (0) | 2023.07.14 |
---|---|
[Android] FirebasePerformance란? FirebasePerformance 사용법 (0) | 2023.07.13 |
[Android] 코루틴 채널(Channel)이란? (0) | 2023.06.29 |
[Android] Parcelize란? (0) | 2023.06.21 |
[Android] Espresso Web이란? 웹뷰 UI 테스트 예제 (0) | 2023.04.15 |