테스트
테스트의 단위
유닛(Unit)테스트
- 코드의 가장 작은부분(보통 메서드)을 테스트하는 것
- 외부의 리소스(네트워크/데이터베이스)는 배제하고 테스트를 수행할 수 있어야 함
- 외부환경이 테스트의 결과를 좌우하면 안되므로..
- 유닛테스트는 테스트목표가 간단하고 명확해야 한다!
- ex) 메서드의 유닛테스트 - 테스트용 입력값을 주었을 때, 출력값이 정확한가?
- 유닛테스트 단위의 예시
- DNote의 객체를 Note의 객체로 잘 변환하는가?
- (어떤 데이터소스이든 상관없이) 데이터소스로부터 pageId를 가진 데이터를 가져와서 Page 객체로 잘 변환하는가?
- 레포지토리로부터 가져온 Page를 가공하여 LiveData로 제대로 출력되고 있는가?
통합(Integration)테스트
- 각 기능(유닛)들이 상호작용하고 잘 동작하는지를 테스트하는 것
- 외부의 리소스가 결합된 유닛테스트라고 이해하면 될듯
- 통합테스트는 외부의 리소스와 결합하여 기능이 동작하는가를 확인하는게 목적이므로
- 통합테스트 단위의 예시
- 데이터베이스로/서버로부터 pageId를 가진 데이터를 가져와서 Page 객체로 잘 변환하는가?
기능(Functional / Acceptance)테스트
- 애플리케이션 / 소프트웨어가 제대로 동작하는지 완전한 기능을 테스트하는 것
- QA분들이 해주시는 하나의 TC라고 생각하면 될듯
(유닛)테스트코드
- 일반적으로 테스트코드를 작성한다는건 유닛테스트를 의미함
- 통합/기능테스트의 코드를 작성하기도 하지만, 들어가는 공수대비 관리포인트가 많기때문에...
테스트코드 뭐가 좋은가?
- 작성한 비즈니스 코드가 정상적으로 동작하는지를 명확하게 반복적으로 확인할 수 있음
- 코드를 변경했을 때, 기존에 동작하던게 문제없이 동작하는지를 검증할 수 있음
- 테스트를 위해서 코드를 바꾸다보면... 결과론적으로 코드의 구조가 개선됨 (단일책임원칙 등..)
테스트코드 단점은?
- 테스트코드도 결국은 코드이기 때문에 작성에 공수가 들어감
- 스펙변경 등으로 테스트코드가 변경되어야 하는 상황이 발생.... 관리해주어야 하는 번거로움ㅠㅠ
- (코드 구조가 꼬여있어서) 테스트를 위해 코드의 전체 구조를 바꿔야하는 아이러니한 상황이 발생하기도...
어떻게 짜면 좋을까?
- 반드시 이렇게 짜야한다라는 건 없음. 목적/단위에 맞게 테스트를 하도록 짜면 됨
- 유닛테스트에서는 외부 리소스/환경에 영향을 받으면 안되므로, Mock / Spy / Dummy 객체를 이용
- 테스트코드 패턴으로 Given - When - Then 패턴이 존재하며, 막막한 테스트코드를 짜는데 가이드가 될 수 있음
- Given(준비) : 테스트를 위한 준비과정. Mock, 더미 데이터 등 테스트에 사용하는 값들을 초기화하고 준비하는 단계
- When(실행) : 검증할 코드를 실행하는 단계. 유닛테스트에서는 보통 메서드를 테스트하므로 메서드를 실행하는 한줄만 들어감
- Then(검증) : 메서드를 수행한 후 받은 출력이 예상과 같은지를 검증하는 단계
@Test
fun given_목레포지토리_when_파트너데이터_조회_then_데이터불러오기_성공() {
val partnerData = makeDummyData()
given("목레포지토리에 파트너데이터 지정") {
Mockito
.`when`(mockSettingRepository.getPartnerInfo())
.thenReturn(Single.just(partnerData))
}
`when`("뷰모델 fetchPartnerData()") {
viewModel.fetchPartnerData()
}
then("구독한 LiveData로 제대로 된 데이터가 넘어오는지 확인") {
Assert.assertEquals(partnerData, viewModel.partnerData.getOrAwaitValue())
}
}
안드로이드에서의 테스트
테스트환경
- 안드로이드에서는 2가지의 테스트환경이 있음
JUnit Test
- JVM 기반의 테스트툴인 JUnit을 이용하여 테스트를 수행하는 환경으로 안드로이드 환경과는 분리되어 있음
- 단순 JVM 기반이기 떄문에 속도가 매우 빠름
- 안드로이드 종속적인 테스트는 수행할 수 없음
- ex) Context로부터 데이터를 얻어온다거나... 이런 테스트는 불가능.
- 단, 테스트 수행시에 안드로이드 기반의 객체들을 기본값으로 셋팅하도록 지정할 수는 있음
android { ... testOptions { unitTests.returnDefaultValues = true } ... }
Instrumented Test
- 안드로이드 환경(에뮬레이터, 실기기)에서 테스트를 수행
- JUnit Test에 비해 실제로 앱을 빌드하여 수행하는것과 동일하므로 속도가 느림
- 안드로이드 종속적인 테스트를 수행할 때 이용.
- ex) Room을 이용한 테스트(Room은 Context를 필요로 하니까)
테스트하기 어려운 상황들 우회
LiveData
- LiveData.observeForever() 를 이용하여 라이프사이클 상관없이 값을 구독하도록 함.
// LiveDataTest.kt
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS
): 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)
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
// 실제 사용
@RunWith(JUnit4::class)
abstract class BaseTest {
// LiveData를 테스트하기 위해 꼭 추가해주어야 하는 Rule
// androidx.arch.core.executor.testing.InstantTaskExecutorRule
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
...
@Test
fun 테스트() {
then("page load fail") {
Assert.assertNotNull(pageViewModel.error.getOrAwaitValue())
}
}
}
RxJava
- UnitTest에서는 Rx의 Scheduler가 원활하게 동작하지 않는다. (Schedulers.io), AndroidSchedulers.mainThread())
- 해당 스케쥴러를 Schedulers.trampoline() 으로 동작하도록 바꿔주어 문제를 해결할 수 있음
// RxSchedulersRules.kt
class RxSchedulersRules : TestRule {
override fun apply(
base: Statement,
description: Description
): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
RxJavaPlugins.setIoSchedulerHandler {
Schedulers.trampoline()
}
RxAndroidPlugins.setInitMainThreadSchedulerHandler {
Schedulers.trampoline()
}
try {
base.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
}
}
}
// 실제 사용
@RunWith(JUnit4::class)
class BaseTest {
// 이 Rule만 선언해주면 됨.
@get:Rule
val testSchedulerRule = RxSchedulersRules()
...
}
안녕하세요? 개발은 정말 손이 많이 가는 작업인 것 같습니다. 블러그 잘 보았습니다.
답글삭제