Android

[안드로이드] ViewModel 테스트하기 : [우아한테크코스 5기 AN_베르]

베르_최성훈 2023. 7. 22. 18:08

이전에 Presenter 테스트 작성으로 테스트의 중요성을 알아봤다.

 

https://seonghoonc.tistory.com/20

 

Presenter 테스트 작성하기 (안드로이드 MVP) : [우아한테크코스 5기 AN_베르]

들어가기 전에 이 글은 MVP 패턴 적용이나 단위 테스트에 대한 경험이 없다면 이해하는데 어려움이 있을 수 있습니다. 테스트가 필요한가? 장바구니 주문 미션 2단계 제출할 때까지 Presenter 테스

seonghoonc.tistory.com

 

또한 MVP 를 MVVM으로 리팩터링하는 과정도 거쳤다.

 

https://seonghoonc.tistory.com/21

 

[안드로이드] MVP 패턴 MVVM 패턴으로 리팩터링하기

MVP 패턴으로 작성한 단순한 화면을 MVVM 패턴으로 바꿔보자! + 버튼을 누르면 1씩 증가하고 - 버튼을 누르면 1씩 감소하는 단순한 앱을 만들어보자. 먼저 xml 파일을 작성해서 화면을 그렸다. MVP 패

seonghoonc.tistory.com

 

이제 ViewModel 테스트 작성해서 기능이 정상 동작하는지 알아보자.

 

먼저 이전과 똑같은 Counter class 를 가져왔다.

 

class Counter(initialCount: Int = DEFAULT_COUNT) {

    private var count = initialCount

    fun add(): Int {
        return ++count
    }

    fun sub(): Int {
        return --count
    }

    companion object {
        const val DEFAULT_COUNT = 0
    }
}

 

Domain Model Unit Test

먼저 Counter 도메인 모델 단위 테스트부터 작성하자.

 

Junit4 와 AssertJ 를 사용했다. Junit5 를 사용해도 된다.

 

class CounterTest {

  
@Test
    fun `카운터가 0일 때 1 증가시키면 카운터는 1이다`() {
        // given
        val counter = Counter()

        // when
        val actual = counter.add()

        // then
        val expected = 1
        assertThat(actual).isEqualTo(expected)
    }

    @Test
    fun `카운터가 2일 때 1 감소시키면 카운터는 1이다`() {
        // given
        val counter = Counter(2)

        // when
        val actual = counter.sub()

        // then
        val expected = 1
        assertThat(actual).isEqualTo(expected)
    }
}

 

 

이제 Counter 를 사용한 ViewModel 을 테스트해보자.

MainViewModel

class MainViewModel : ViewModel() {

    private val counter = Counter()

    private val _count: MutableLiveData<Int> = MutableLiveData<Int>(0)
    val count: LiveData<Int> = _count

    fun plusCount() {
        _count.value = counter.add()
    }

    fun minusCount() {
        _count.value = counter.sub()
    }
}

 

매우 간단한 구조의 ViewModel 이다.

 

이를 테스트하려면 어떻게 해야할까?

 

먼저 안드로이드 의존성을 가진 LiveData 테스트를 위해서 Rule 을 추가해야하며 나머지는 크게 다르지 않다.

testImplementation("androidx.arch.core:core-testing:2.2.0")

 

class MainViewModelTest {

    private lateinit var vm: MainViewModel

    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setUp() {
        vm = MainViewModel()
    }

    @Test
    fun `count 를 증가시키면 count 가 1 이다`() {
        // given

        // when
        vm.plusCount()

        // then
        assertThat(vm.count.value).isEqualTo(1)
    }

    @Test
    fun `count 를 감소시키면 count 가 -1 이다`() {
        // given

        // when
        vm.minusCount()

        // then
        assertThat(vm.count.value).isEqualTo(-1)
    }
}

 

그런데 실제 ViewModel 이라면 어떨까 repository 에 값을 요청하는 함수 등으로 비동기로 처리하고 있지 않을까?

그러면 어떻게 테스트 해야할까?

 

좀 더 복잡한 과정이 필요하다.

 

repository 패턴을 사용하기 때문에 목킹도 필요하다.

 

목킹에는 mockk 라이브러리를 사용

coroutine test 를 위해 implementation 추가

 

//mockk
testImplementation("io.mockk:mockk-android:1.13.5")

//coroutine test
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")

 

...
    private lateinit var vm: SomeViewModel
    private lateinit var someRepository: SomeRepository

    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    @OptIn(ExperimentalCoroutinesApi::class)
    @Before
    fun setUp() {
        Dispatchers.setMain(UnconfinedTestDispatcher())
        someRepository = mockk()
        vm = SomeViewModel(someRepository)
    }
    
    @OptIn(ExperimentalCoroutinesApi::class)
    @After
    fun finish() {
        Dispatchers.resetMain()
    }

 

이렇게 해서 비동기코드를 동기적으로 테스트 가능해졌다.

 

@Test
    fun `어떤 것을 받아온다`() {
        // given some ID 를 가지고 있다.
        val someId = 1L
        val fakeSomething = Something(someId, "베르", "최성훈")
        coEvery { someRepository.loadSomething(any()) } returns fakeSomething

        // when something 을 요청한다.
        vm.loadSomething(someId)

        // then something 을 받는다.
        assertThat(vm.something.value).isEqualTo(fakeSomething)
    }

이때 유의할 점은 코루틴을 사용하는 메서드를 목킹할때는 coEvery 를 사용해야 한다.

 

이렇게 하면 내가 정의한 ViewModel 이 정상 작동하는지 테스트로 확인할 수 있다!

 

MVVM 의 ViewModel 테스트가 MVP 의 Presenter 테스트보다 훨씬 간단한 구조를 띄어 가독성이 높았다!