Study/object

[엘레강트 오브젝트] 2-8장 모의 객체(Mock) 대신 페이크 객체(Fake)를 사용하세요.

에디개발자 2021. 9. 5. 07:00
반응형

나를 닮았다고 한다...

이 글은 엘레강트 오브젝트 새로운 관점에서 바라본 객체지향 도서를 보며 스터디한 글입니다.

책에서 주장하는 내용을 정리하였으며 예제들은 모두 코틀린 코드로 변환하여 작성하였습니다.

 

목차

  1. Mock은 Bad Practice
  2. Fake 객체 사용
  3. 정리

1. Mock은 Bad Practice

TDD 관련 블로그, 영상을 살펴본다면 Mock 방식은 매우 위험한 방식이라고 소개합니다. 이유는 너무 간편하게 사용할 수 있어 남용하기 편하기 때문입니다. Mock이 많아진다면 그만큼 객체 간 강한 결합력을 가지고 있다는 말입니다. 

 

이 책에서도 Mock에 대해서는 비판적이게 평가하고 있습니다. 예제를 통해 살펴보겠습니다.

class Cash(
    private val exChange: Exchange,
    private val cents: Int
) {
    fun `in`(currency: String) {
        Cash(
            this.exChange,
            (this.cents * this.exChange.rate("USD", currency)).toInt()
        )
    }
}

interface Exchange {
    fun rate(origin: String, target: String): Double
}

Cash 클래스는 속성값으로 Exchange 객체를 받습니다. Exchange 객체는 외부 통신하여 환율 정보를 얻어오는 목적을 가지고 있습니다.

 

Mock으로 테스트 코드를 작성해보겠습니다. 

@Test
fun `cash test`() {
    // Given
    val exchange = Mockito.mock(Exchange::class.java)
    Mockito.doReturn(1.15)
        .`when`(exchange)
        .rate("USD", "EUR")

    // When
    val dollar = Cash(exchange, 500)
    val euro = dollar.`in`("EUR")

    // Then
    assertThat(euro.toString()).isEqualTo("575")

}

Mock 처리를 위해 장황한 코드가 작성된 것을 확인할 수 있습니다. 

 

또 다른 문제점

다른 문제점은 Exchange의 기능을 추가했을 경우에도 발생합니다.

Exchange Interface에 target만 파라미터로 넘기는 메서드를 추가합니다. 그리고 Cash 클래스의 in 메서드에 curreny속성값이 "USD"로 받을 경우 target만 파라미터로 넘기도록 수정해보겠습니다.

class Cash(
    private val exChange: Exchange,
    private val cents: Int
) {
    fun `in`(currency: String): Cash =
        if (currency == "USD") {  // 분기
            Cash(
                this.exChange,
                (this.cents * this.exChange.rate(currency)).toInt()
            )
        } else {
            Cash(
                this.exChange,
                (this.cents * this.exChange.rate("USD", currency)).toInt()
            )
        }

    override fun toString(): String {
        return "$cents"
    }


}

interface Exchange {
    fun rate(origin: String, target: String): Double
    fun rate(target: String): Double  // 추가
}

 

그리고 아래의 테스트 코드를 실행해보겠습니다.

@Test
fun `cash test`() {
    // Given
    val exchange = Mockito.mock(Exchange::class.java)
    Mockito.doReturn(1.15)
        .`when`(exchange)
        .rate("USD", "EUR")
        
    // When
    val dollar = Cash(exchange, 500)
    val euro = dollar.`in`("USD")

    // Then
    assertThat(euro.toString()).isEqualTo("575")

}

결과는 당연히 실패로 떨어집니다. 이유는 Mock처리에서 target만을 메서드 받는 경우에 대해서 작성하지 않았기 때문입니다. 이처럼 기능이 추가될 때마다 테스트 코드의 Mock을 하나하나 찾아서 수정해야하는 불편한 일이 생깁니다. 

 

2. Fake 객체 사용

위에서 Mock 처리했던 내용을 interface의 내부 클래스 Fake 로 생성하여 사용합니다. 

interface Exchange {
    fun rate(origin: String, target: String): Double
    fun rate(target: String): Double

    // fake 
    class Fake: Exchange {
        override fun rate(origin: String, target: String): Double {
            return 1.15
        }

        override fun rate(target: String): Double {
            return this.rate("USD", target)
        }

    }
}

그리고 위와 같은 테스트 코드를 동일하게 수행하면 성공하는 것을 확인할 수 있습니다. 

이처럼 Fake 객체를 사용한다면 interface에서 새로운 기능이 추가될 때 compile 시점에서 바로 확인할 수 있습니다. 또한 작성된 테스트 코드를 모두 찾을 필요도 없어집니다. 

 

3. 정리

  Mock Fake
테스트 코드 장황해진다. 깔끔하다.
추가 기능 테스트 코드 수정 필요 O 테스트 코드 수정 필요 X
실패 추적 시점 테스트 코드 실행 후 확인 가능 Compile 레벨에서 즉시 확인 가능
난이도 테스트 코드에서 캡슐화된 정보를 알아야하므로 작성하기 어렵다. 테스트 코드에서 캡슐화된 정보를 알 필요가 없으므로 작성하기 쉽다.

 

이 책의 2.3 항상 인터페이스를 사용하세요. 에서 강조한 느슨한 결합을 위해서는 인터페이스를 사용하라는 내용에 연장선입니다. 느슨한 결합을 할 수 있기 때문에 이처럼 인터페이스 내부에서 Fake 객체를 만들어 편리하게 사용할 수 있습니다.

반응형