2021년 10월 16일 토요일

[안드로이드/테스트] 테스트의 기본개념 & 안드로이드에서의 테스트

 

테스트

테스트의 단위



유닛(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()
    ...
}

참고문서

댓글 1개:

  1. 안녕하세요? 개발은 정말 손이 많이 가는 작업인 것 같습니다. 블러그 잘 보았습니다.

    답글삭제