MockK 라이브러리로 프레젠터 테스트하기

Mendel·2023년 11월 7일
0

안드로이드

목록 보기
2/7

mock 이란?

가짜 객체를 말한다. 그렇다면 프레젠터를 테스트하는데 가짜 객체는 왜 필요할까?

프레젠터를 테스트하는데 있어서, 서버나 디비에 직접 접근하는 등의 불필요한 일은 배제하고 본질적인 로직을 테스트하기 위해.

위의 말이 아직 너무 어렵다... 일단 가짜 객체가 필요한 이유는 이따가 알아보고, 그래서 구체적으로 뭘 테스트하고 싶다는 것인가?

프레젠터 테스트에서 하고 싶은 테스트는 그래서 무엇인가?

프레젠터 인터페이스를 구현해서 우리가 만든 실제 프레젠터 클래스의 객체가 우리가 원하는 대로 동작을 하는지
(프레젠터의 메소드를 실행했을때, 뷰의 메소드를 적절한 시점에 제대로 실행시키고 있는지, 뷰의 메소드를 실행시킬때 건네주는 인자가 제대로된 데이터인지, 프레젠터가 가진 레포지토리의 메소드를 제대로 실행시키고 있는지 등의 동작을 테스트하고 싶은 것이다)

이제 뭘 테스트하고 싶은 건지는 알았다. 그런데 가짜 객체는 왜필요하고 언제 써야 하는가??

가짜 객체가 필요한 이유

프레젠터를 테스트하는데 있어서 실제 뷰의 실행 로직이나 디비의 변경 내용이 중요할까? 중요하지 않다. 우리는 프레젠터가 제대로 뷰의 메소드를 실행하는지 여부와 프레젠터가 가공해서 뷰에게 건내주는 인자의 내용등이 궁금할 뿐이다.

때문에, 실제 레포지토리(실제 디비에 접근하는 레포지토리 클래스) 클래스를 프레젠터 구현체의 생성자에 넘겨줘서 프레젠터 객체를 만들고, 이 객체로 테스트를 수행하기 위해 프레젠터 메소드를 실행하면, 프레젠터 테스트와 무관한 것들까지 테스트하는 통합 테스트가 되어 버리고 이것은 더이상 프레젠터만의 단위 테스트가 아니게 된다.

프레젠터만을 위한 혹은 실제 디비 변경을 하지 않기 위해... 또한,
순수한 프레젠터만의 단위 테스트를 수행하기 위해서는 프레젠터에게 주입해주는 뷰와 레포지토리가 가짜여야 한다는 것이다. 이 가짜를 만들기 위해 사용하는 것이 mockK 라이브러리이다.

이제 실제 테스트 코드를 작성하기 위한 메소드들을 알아보자

가짜 객체 만들기 - mockk() 혹은 spyk() 사용

우선 여기서는 mockk()만 다루자. 일반적으로 가짜 객체는 @Before로 테스트 메소드를 수행하기 전마다 새로 만들어준다.

참고) spyk는 private 메소드를 테스트할 수 있는 가짜 객체를 만들고 싶을 때 사용하는 방법이라고 한다. 아직 실제로 써본 적은 없고 나중에 필요하면 그때 학습하면 좋을 것 같다.

    val view = mockk<Contract.View>() 
    val repository = mockk<RepositoryInterface>() 

위처럼 실행하면, 해당 타입의 public 메소드들만을 가진 객체가 만들어진다. 그리고 이 객체의 메소드들은 아직 정의되지 않은 상태이다. 때문에 정의되지 않은 이 mockk객체의 구성 메소드가 호출된다면 예외가 발생한다.

이제부터는 본격적으로 given - when - then 으로 생각해보자.

각 단계에서 사용해야 하는 메소드들을 아래에서 소개할 것이다.

given 단계

가짜 객체의 메소드 정의하기 - every 사용

우리가 위에서 만든 가짜 객체는 메소드가 정의되지 않은 빈 껍데기에 불과하다. 가짜 객체의 메소드를 정의하는 방법은 every를 사용하면 된다. 이때, every는 크게 총 네 종류가 있다.

  • every { view.정의할메소드() } returns 1
    해당함수는 앞으로 1을 리턴하는 메소드가 된다.
  • every { view.정의할메소드()} throws RuntimeException()
    해당함수는 앞으로 예외를 발생하는 메소드가 된다.
  • every { view.정의할메소드() } just Runs
    해당함수는 앞으로 아무 로직 없이 단순히 실행만 되고 끝나는 Unit을 반환하는 메소드가 된다.
  • every { view.정의할메소드() } answers{...}
    해당함수는 앞으로 answers 뒤에 따르는 람다에 정의한 로직을 수행하고 람다 안의 마지막 줄을 반환하는 메소드가 될 것이다. 즉, 람다가 반환내용이 된다는 것이다.

메소드를 정의하고 싶은데, 그 메소드에 매개변수가 존재한다면? any() 혹은 slot()을 사용하면 된다. any는 무슨 값이든 상관없다는 의미로 그냥 매개변수를 채우기 위한 인자로 사용하면 된다. slot은 해당 메소드가 호출될때마다 해당 매개변수에 무슨 인자가 들어왔는지 저장하고 싶을때 사용한다.
자세한 사용 코드는 맨 마지막에 종합적으로 다루겠다.

when 단계

테스트하고자 하는 프레젠터의 메소드를 호출해보는 단계다.

then 단계

검증하기 - verify 사용

위의 when 단계에서 수행한 프레젠터의 메소드로 인해서 무슨일이 발생했는지, 가짜 객체의 어떤 메소드가 호출됐는지 등을 알고 싶다면 사용하는 것이 바로 verify다. 아래와 같이 4가지 종류가 있다.

  • verify { view.정의할메소드() }
    해당함수가 1번 이상 실행되었는지 확인
  • verify { view.정의할메소드() wasNot Called}
    해당함수가 실행되지 않았음을 확인
  • verifyOrder { ... }
    { ... } 안의 함수들이 순서대로 수행되었는지 확인
  • verifySequence { ... }
    { ... } 안의 함수들이 순서상관없이 모두 수행되었는지 확인

메소드가 특정 인자를 받아서 실행된 적이 있는지 구체적인 경우를 테스트하고 싶다면, 메소드 안에 특정 값들을 채워넣어서 verify를 정의하면 되고, 아무값을 받든 상관없이 호출됐는지만 궁금하다면, any()로 메소드의 인자를 채워넣어서 verify를 정의하면 된다.
만약 특정 횟수만큼 호출됐는지 궁금하다면, verify(exactly == 원하는카운트횟수) 로 exactly를 지정해주면 된다.

실전 테스트 예시

mock 객체 만들기

    private lateinit var view: ReservationListContract.View
    private lateinit var presenter: ReservationListContract.Presenter
    private lateinit var ticketsRepository: TicketsRepository

    @Before
    fun init() {
        // 뷰와 레포지토리를 가짜 객체로 만들고, 테스트하고자 하는 프레젠터에 넣는다.
        view = mockk()
        ticketsRepository = mockk()
        presenter = ReservationPresenter(view, ticketsRepository)
    }
    @Test
    fun 예약목록을_불러와서_뷰의_아이템을_업데이트한다() {
        // 미리 준비한 mockTickets를 반환하도록 가짜 레포지토리 객체의 allTickets() 메소드를 정의한다.
        every { ticketsRepository.allTickets() } returns mockTickets 
        
        // view.updateItems가 받게될 인자를 기억하기 위해 slot객체를 활용한다.
        val slot = slot<List<TicketsItemModel>>() 
        // 가짜 view 객체의 updateItems() 메소드는 단순하게 아무로직없이 실행되기만 하는 메소드라고 정의한다. 또한, 이 메소드가 받은 인자는 앞으로 slot에 기억시킬 것이다. 
        every { view.updateItems(capture(slot)) } just Runs



        // 실행.. 이게 바로 이번에 테스트하고자하는 프레젠터의 메소드다.
        presenter.loadTicketsItemList()

      

        // slot.captured를 사용하면 기억했던 값을 불러낼 수 있다. 
        val actual = slot.captured.map { it.ticketsState }
        val expected = mockTickets.map { it.asPresentation() }
        
        // 레포지토리에서 불러온 예약 목록을 뷰의 updateItems에 제대로 넘겨서 호출했는지 검사
        assert(actual == expected) 
    }

참고링크

https://mockk.io/
https://sabarada.tistory.com/191
https://team.mycaro.co.kr/android-mockk를-이용하여-unittest-작성하는-법-w-카로-로그인화면/

profile
이것저것(안드로이드, 백엔드, AI, 인프라 등) 공부합니다

0개의 댓글