Unit Testing (2) - 5장 : Mock과 테스트 취약성

강지혁·2022년 9월 4일
0

Unit Testing

목록 보기
3/3

Unit Test 5장의 내용을 정리한 글입니다.


Test Double

스턴트 대역 (Stunt Double)에서 파생한 말로, 객체를 테스트 하는 상황에서, 의존하는 객체들을 대신해 동작하는 대역 객체들을 일컫는 말입니다.

  • Mock Group : 외부로 나가는 상호작용을 모방
    • DB에 값을 쓰는 것처럼, 외부 의존성에 변경을 만드는 경우 사용합니다.
      • mock
      • spy (handwritten mock)
      • spy 객체의 경우 단순 mock 기능 뿐만 아니라, elapsed time , logging 등 우리가 원하는 동작을 추가적으로 사용하게끔 만들기도 합니다.
  • Stub Group : 내부로 들어오는 상호작용을 모방
    • DB에서 값을 읽는 것처럼, 부작용이 없는 경우 사용합니다.
      • dummy (값에 의미 X)
      • stub (시나리오에 따라 원하는 값을 반환)
      • fake (아직 존재하지 않는 의존성을 대체할 때 씀, 그냥 통상적으로 우리가 아는 stub)

Mock vs Stub

Stub으로는 상호작용을 검증하지 말자

  • SUT에서 Stub을 호출하는 것은, 최종 결과가 아니라, 최종 결과를 산출하기 위한 수단일 뿐입니다.
    • 이러한 메소드의 호출을 검증하는 것은 테스트 취약성을 높이고 + 의미도 없음
    • 이것이 바로 과잉 명세 (overspecification)
      • example code
        import org.junit.jupiter.api.Assertions.*
        import org.junit.jupiter.api.Test
        import org.junit.jupiter.api.extension.ExtendWith
        import org.mockito.Mockito
        import org.mockito.Mockito.`when`
        import org.mockito.junit.jupiter.MockitoExtension
        import org.mockito.kotlin.*
        
        @ExtendWith(MockitoExtension::class)
        internal class StubTest {
            @Test
            fun `Do not verify Stub method call`() {
                // given
                val stub =mock<IDB>()
        whenever(stub.getNumberOfUsers()).thenReturn(10)
        
                // when
                val sut = Controller(stub)
                val result = sut.createReport()
        
                // then
                assertEquals(Report(10),result)
        		// BAD LINE!!
        		verify(stub,times(1)).getNumberOfUsers() 
            }
        }
        
        interface IDB {
            fun getNumberOfUsers(): Int
        }
        
        class Controller(
            private val db: IDB
        ) {
            fun createReport(): Report {
                return Report(db.getNumberOfUsers())
            }
        }
        
        data class Report(val id: Int = 0)
    • stub 객체에 변경사항이 생기거나 말거나, 우리는 sut의 동작이 기대에 부응하는지에 집중하여 테스트를 작성하고, 확인해야 합니다.

Mock-Stub은 CQS의 Command-Query와 같다

  • 앞선 소주제의 연장선상입니다.
    • Mock은 Command, 부작용을 만드는 행위
    • Stub은 Query, 다른 객체로부터 값을 빌려오는 행위
  • TMI: CQRS는 CQS의 확장 버전으로, CQS가 메소드 레벨에서의 분리인 반면 CQRS는 시스템이나 객체 수준에서의 분리입니다.

"구현 세부사항"과 "식별할 수 있는 동작"은 어떻게 다른가?

우리는 식별할 수 있는 동작에 관심을 가지고, 구현 세부사항을 이들과 분리해야 합니다.
그래야만 리팩토링 내성을 기르고, 변경에 유연한 코드를 작성할 수 있습니다.
그렇다면 이 둘을 어떻게 구분해낼 수 있을까요?

식별할 수 있는 동작이란..

  1. Operation을 노출 → 계산을 수행하거나, 부작용을 초래함
  2. State를 노출 → 시스템의 현재 상태

잘 설계된 API & Capsulizing

  • 캡슐화는 코드 복잡도 증가에 대처하는 거의 유일한 해결책입니다.
  • 코드베이스가 하는 일을 명확히 해야 실수할 가능성이 없어지기 때문!
  • 이는 단위 테스트의 목적과도 동일하다.
  • ❗️갑자기 이 소리를 하는 이유가 뭐냐면..
    • 구현 세부사항을 잘 숨기는 것이, 즉 코드를 애초에 그렇게 잘 짜는 것이, 단위 테스트의 품질도 높여줄 수 있음을 책에서는 강조하고 있습니다.
    • API를 잘 설계해야 좋은 테스트도 나온다는 것이죠 ..

비교 Table

식별할 수 있는 동작구현 세부 사항
공개좋음나쁨
비공개비공개일 수가 없음좋음

Mock과 Test 취약성

캡슐화를 잘 지키고, 리팩토링 내성 (변경사항 내성)을 잘 지킨다..
클린 아키텍처에서 살펴본 내용이랑 굉장히 닮아 있습니다.
책에서도 Hexagonal Architecture를 언급하고 있네요 :)

Hexagonical Architecture

(헥사고날 아키텍처에 대한 너무 좋은 출처가 있어 공유해봅니다.)

  • 도메인 - 애플리케이션 서비스의 분리
    • 도메인 계층에는 핵심 비즈니스 로직, 애플리케이션 계층에는 비즈니스 유즈케이스
  • 테스트 관점에서는, 계층의 의존성을 아래와 같이 나눠볼 수 있겠습니다.
    • 애플리케이션 간의 통신을 위한 어댑터
    • 애플리케이션 내부 통신을 위한 의존성

시스템 내부 통신 & 시스템 간 통신, 그리고 Mock

  • 언제 Mock 을 쓰면 될까요?
  • 즉, 어떤 의존성에 대해 Mock을 사용하면 좋을까요?

"테스트 간" 공유하는 "외부 의존성"에 Mock을 사용하자

의존성에는 두 가지 종류가 있습니다.

  • 테스트 간 간섭이 일어나는, 공유 의존성 (Mutable)
  • 각 객체 내부에서 private한, 비공유 의존성

공유 의존성은 다시 두 가지로 나뉩니다.

  • 시스템 내부에 존재하는 (intra-system) 의존성
  • 시스템 외부에 존재하는 (inter-system) 의존성

테스트 격리의 측면에서, 런던파는 모든 공유 의존성을, 고전파는 "프로세스 외부의" 공유 의존성을 mocking 합니다.

그러나 두 분파 모두 리팩토링 내성의 측면에서는 관리가 힘든 (brittle) 테스트를 만들어낼 위험이 있습니다.

Mocking 하지 말아야 할 외부 프로세스 의존성

프로세스 외부에 있다고 해서, Mock을 무조건 써야 하는 것은 아닙니다.

만약 클라이언트가 접근을 못하는, 오직 앱만 접근 가능한 의존성이라면,
=> 이것은 구현 세부 사항이지 식별 가능한 동작(계약)이 아닙니다.

즉 언제든 갈아끼워질 준비가 되어있어야 합니다.
Mocking은 기본적으로 테스트에 구현 세부 사항을 노출하는 행위이기 때문에,
이러한 구현 세부 사항에 대해서 과도하게 Mocking을 하는 행위는 좋지 않습니다.

💡 결론: 시스템 간 통신 (앱 경계를 넘는 통신)과 해당 통신의 부작용이 외부 환경에서 클라이언트에게 보일 때 Mock을 사용하자!


reference:
when-to-mock (Unit Testing 저자 블로그)

0개의 댓글