우아한테크코스

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

베르_최성훈 2023. 6. 23. 22:05

들어가기 전에

이 글은 MVP 패턴 적용이나 단위 테스트에 대한 경험이 없다면 이해하는데 어려움이 있을 수 있습니다.

 

테스트가 필요한가?

장바구니 주문 미션 2단계 제출할 때까지 Presenter 테스트 의 필요성을 인지하지 못했다. 리팩터링 할 때마다 테스트 리팩터링이 계속해서 필요했다. 또한, 기능을 다 구현했는데 하나하나 테스트를 짜야한다는 것이 그저 숙제로 느껴졌다. 시간이 부족하다는 이유로 기존에 작성했던 테스트를 전부 주석 처리하고 제출하는 만행을 저질렀다.

리뷰어님이 다음과 같은 메세지 를 남겨주셨다.

도메인 관련 클래스를 잘 분리하고 테스트 코드까지 잘 작성해주셨습니다.
다만 안드로이드와 관련된 코드들은 테스트가 작성되지 않은게 아쉬웠습니다.

그러자 내가 테스트를 잘 못짜서 필요성을 인지하지 못하는 것이 아닐까 생각이 들었다. 팀 프로젝트로 가기 전 마지막 레벨 2 미션이 끝나기 전에 마음을 다 잡고 이번 모든 화면의 Presenter 테스트를 전부 다시 짜보기로 했다.


테스트 명세

테스트를 작성하기 전에 가장 먼저 Presenter 가 어떤 기능을 수행하는지 정리해야했다.
수업 시간에 바운 Gherkin 문법을 사용해서 given, when, then, and 로 나누어 작성했다.

Gherkin 이란 ?
BDD 기반의 테스트 도구인 Cucumber에서 제안된 테스트 문법이다.

화면 별로 나누어 각 Presenter 가 수행해야하는 기능을 다음과 같이 작성했다.

  1. GIVEN : 기능을 수행할 수 있는 상태임을 명시한다.
  2. WHEN : 기능을 수행을 명시한다. (Presenter 에게 요청한다)
  3. THEN : 기대하는 결과 검증을 명시한다.
  4. AND : 추가적으로 검증하고 싶은 결과를 명시한다.

테스트 코드 양이 많아서 주문 목록 불러오기, 장바구니 선택 해제하기 만 설명하겠다.

작성된 다른 테스트가 궁금하다면 아래 PR 에서 확인할 수 있다.

 

https://github.com/woowacourse/android-shopping-order/pull/35/files/f60ef143e2b4cda04c1612ce86fa5422ab03c868..ca4345c923ca2f5fe8d47e701c326fbdaa28928a

 

[베르] 2단계 재화 미션 제출합니다. by SeongHoonC · Pull Request #35 · woowacourse/android-shopping-order

안녕하세요 토리! 먼저 이전 단계 제출한 코드가 마음에 들지 않아서 처음부터 만들었습니다. 코드 변경 사항이 많이 있어 혼란스러우실 것 같습니다.. 다음 변경사항 링크를 꼭 확인하여 주세

github.com

1. 주문 목록 불러오기

주문 목록 화면

GIVEN 주문 목록을 불러올 수 있는 상태다.
WHEN 주문 목록 불러오기 요청을 보낸다.
THEN 주문 목록이 노출된다.

2. 장바구니 선택 해제하기

GIVEN 장바구니 아이템의 선택 상태를 변경할 수 있는 상태이다.
WHEN 장바구니 아이템 선택 상태 변경 요청을 보낸다.
THEN 전체 선택 여부를 확인한다.
AND 선택된 상품 전체 가격을 업데이트 한다.
AND 선택된 상품 전체 개수를 업데이트 한다.

테스트 더블 

테스트 명세를 작성했으면 이제 테스트 코드를 작성할 차례다. 테스트를 작성하는 데는 여러 방법이 있는데 나는 테스트 더블을 사용했다.

테스트 더블 참고 (Tecoble)
https://tecoble.techcourse.co.kr/post/2020-09-19-what-is-test-double/

테스트 더블은 테스트를 진행하기 어려운 경우에 대신해서 테스트를 진행할 수 있도록 해주는 객체를 말한다.

테스트 더블은 크게 Dummy, Stub, Spy, Mock, Fake 가 있는데 그 중 Stubbing 과 Mocking 을 사용했다.

 

  •  Stubbing
    stub 은 테스트를 위해 프로그래밍된 내용에 대해서만 준비된 결과를 제공하는 객체이다. stubbing 은 테스트할 메서드의 기능을 설정하는 과정이다.
  • Mocking
    mock 은 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍 된 객체이다. mocking 은 설정된 메서드를 통해 기대값을 검증하는 과정이다.

테스트 작성

1. 주문 목록 불러오기

먼저 테스트하기 위해서 mockk 라이브러리를 사용, mock object 로 만들어주었다.

@Before
    fun setUp() {
        view = mockk()
        orderRepository = mockk()
        presenter = OrderListPresenter(view, orderRepository)
    }

 

- GIVEN : 주문 목록을 불러올 수 있는 상태다.

@Test
fun `주문 목록을 불러온다`() {
    // given : 주문 목록을 불러올 수 있는 상태다.
    every {
        view.showOrderList(
            orderModels = OrderFixture.getOrderModels(1, 2, 3),
        )
    } just runs

    every {
        orderRepository.loadOrders(
            callback = any(),
        )
    } answers {
        val callback = args[0] as (List<Order>) -> Unit
        callback(OrderFixture.getOrders(1, 2, 3))
    }

    ...

 상세를 불러올 수 있는 상태를 stubbing 한다.
    1. view.showOrderList() 인자로 id 가 1, 2, 3 인 OrderModel 의 List 일때만 실행되도록 기능을 설정했다.
    2. orderRepository.loadOrders() 의 callback 을 인터셉트 해서 id 가 1, 2, 3 인 Order 의 List를 callback 의 인자로 넘겨주도록 기능을 설정했다.

 

이때, OrderFixture object 는 id 를 인자로 받아서 가짜 객체를 뱉어낸다.

 

- WHEN : 주문 목록 불러오기 요청을 보낸다. 

// when : 주문 목록 불러오기 요청을 보낸다.
	presenter.loadOrderList()

 상품 상세 불러오기 요청 즉 테스트하고자 하는 프레젠터의 메서드를 호출한다.

 

- THEN : 주문 목록이 화면에 노출된다.

 주문 화면에 노출되는지 확인하기 위해 mocking 한다.
 인자로 원하는 결과 OrderModel 리스트가 전달된 view.showOrderList() 가 호출되었는지 검증한다.

 

// then : 주문 목록이 화면에 노출된다.
verify {
    view.showOrderList(
        orderModels = OrderFixture.getOrderModels(1, 2, 3),
    )
}

 

한 가지 다른 예시를 보면서 더 알아보자.

 

2. 장바구니 선택 해제하기 

기본적으로 이런 구조이지만 And 를 사용해야할 때도 있다.

장바구니 아이템을 선택하거나 선택 해제 할 때마다 전체 선택, 전체 가격, 전체 개수가 업데이트 되는지 확인해야한다.

다음 테스트는 given 에서 각 view 의 메서드의 동작을 가정하고
when 에서 선택 상태를 변경했을 때 then 에서 세가지를 검증한다.

 

     @Test
     fun `장바구니 아이템의 선택을 해제한다`() {
        // given : 장바구니 아이템의 선택 상태를 변경할 수 있는 상태이다.
        ...
        
        every { view.showAllCheckBoxIsChecked(false) } just runs
        every { view.showTotalCount(expectedCount) } just runs
        every { view.showTotalPrice(expectedPrice) } just runs
        
        ...
        
        // when : 장바구니 아이템 선택 상태 변경 요청을 보낸다.
        
        presenter.changeProductSelected(
            productModel = CartProductFixture.getProductModel(5),
        )

        // then : 장바구니 아이템 전체 선택을 해제한다.
        verify { view.showAllCheckBoxIsChecked(false) }

        // and : 선택된 상품 전체 가격을 업데이트 한다.
        verify { view.showTotalCount(expectedCount) }

        // and : 선택된 상품 전체 개수를 업데이트 한다.
        verify { view.showTotalPrice(expectedPrice) }
    }

테스트를 작성해보고

 

Presenter 테스트를 처음 작성했을 때 테스트 하나 하나의 코드양이 비대했다.
나도 알아볼 수 없는 테스트가 되어버렸으며 조금만 리팩터링에 유연하지 않았다. 

미리 기능 명세를 작성하고 필요한 기능에 대해 체계적으로 검증하였더니

-> 기존보다 리팩터링에도 유연하고 가독성도 높일 수 있었다.
-> 테스트를 작성하다가 주문 가격을 보여주는 것에 오류가 있었다는 사실도 알고 수정하는 경험을 했다.
-> 기능엔 오류가 없었지만 테스트에 유연하지 못한 구조임을 확인하고 리팩터링한 적도 있었다.

 

그러나 MVP 와 callback 구조를 사용한 탓에 테스트의 가독성을 높이는 데 한계가 있었다.

 



테스트는 코드에 대한 검증이면서 동시에 기능에 대한 명세 이다.
테스트를 작성하지 않으면 내 코드는 신용할 수 없는 코드이자 알 수 없는 코드 로 전락해버린다.

물론 현실적으로 테스트를 작성하지 못하는 순간도 있을 것이다. 
그러나 여유가 된다면 이상을 고민하는 것이 좋은 방향이라고 생각한다.
다음엔 Presenter 테스트로 TDD 해봐야겠다.

이번 테스트 작성을 통해서 Presenter 테스트의 필요성을 느낄 수 있었다.
하지만 다른 교훈이 더 큰 것 같다.

"깊게 고민하거나 경험해보지 않고 판단하지 말자."

 

개발자로서 또는 사람으로서 필요한 생각을 다시한번 느끼면서 마무리한다.