엘레강트 오브젝트의 ch2.8 을 읽으며 실습의 필요성이 느껴져 아래와 같이 진행해보았다.
생각과정이 꽤 많이 들어가서 이상한 코드들이 존재할 수 있으니 많은 첨언 부탁드립니다 🥺
외부 자원( 외부 서버 등)에 의존하는 클래스를 테스트하기 위한 방법은 뭐가 있을까??
- 의존하는 인터페이스에 대한 모의객체 생성하기
- 우리는 이 가짜 객체 중 Mocking 의 단점에 대해 살펴볼 것이다
- 우리는 이 가짜 객체 중 하나인 Fake 객체를 사용 해 볼 것이다.
- 우리는 이 가짜 객체 중 Fake 객체의 장점에 대해 살펴볼 것이다
결론은 → 가능하다면 인터페이스에 대한 Fake 클래스를 작성하여 단위테스트를 합시다!!!
(이 글을 작성할 때는 test double 에 대한 정리가 안되어 있었어서 그런지 혼란스러운 부분이 좀 많은 듯 하다. Test dobule 에 대한 개념도 정리해 두는게 좋을 것 같다 여기엔 안 쓸거임)
- 테스트 클래스에 대하여 Mocking 을 사용하면 안되는 이유 간단 정리 -> 단위테스트 대상 클래스의 내부구현에 의존적인 테스트를 작성하게 된다. 리팩토링 과정에서 테스트 코드 역시 변경해야 한다.
Mocking 을 사용할 때 단점 1 : 테스트 코드가 장황해짐
좌 : Mocking 을 사용
우 : Fake 객체를 사용
Mocking 과 Fake 객체를 사용하여 Cash 클래스에 대한 단위테스트를 작성해 보자 !
(추가) Fake 클래스에 캡슐화가 필요해!!
그런데 위의 테스트 코드를 봤을 때 드는 의문점은, 저 Assert 에서 검증하게 되는 575f 라는 값은 어디서 온 건지 모르겠다는 것이다. 무슨 근거로 575람?? 🤔
이에 대해 조금 더 확실하게 하기 위해, Fake 객체도, 환율에 대한 정보를 "주입" 받도록 작성할 수 있다. 즉, 추가적인 캡슐화와, 작은 클래스를 더 만들고 이를 주입받도록 하여, 테스트하기 쉬운 코드를 만들 수 있다 (라고 생각했는데 맞는지 모르겠다 🤔)
@Test
@DisplayName("Fake 객체를 사용한 테스트")
public void testUsingFake() {
Exchange exchange = new Exchange.Fake();
Cash dollar = new Cash(exchange, 500);
Cash euro = dollar.in("EUR");
// 💥이 때 드는 의문점이 있을 것이다 -> 결과가 575f 인 거는 어떻게 알아? 이 값은 어디서 나온거야??
// -> Fake 를 좀 더 복잡하게 만들어야 한다. 상수 대신, 캡슐화된 비율을 반환하도록
Assertions.assertThat(euro.getCents())
.isEqualTo(575f);
}
@Test
@DisplayName("환율을 주입받은 Fake 객체를 사용한 테스트")
public void testUsingRatio() {
Ratio dollar2EuroRatio = new Dollar2EuroRatio(1.15f);
Exchange exchange = new Exchange.FakeV2(dollar2EuroRatio);
Cash dollar = new Cash(exchange, 500);
Cash euro = dollar.in("EUR");
// FIXME : 의문점!!
Assertions.assertThat(euro.getCents())
.isEqualTo(dollar2EuroRatio.ratio() * 500);
}
그런데 여기서도 또 다시 의문이 들었다
이것 역시 Cash 내부에서 구현이 어떻게 되어있는지 알아서 이렇게 작성되는 것이라는 단점...아닌가????
근데 이 경우는, Cash 의 in() 기능을 테스트하려면, 그 계산 결과 값을 알아야만 테스트가 이루어질 수 밖에 없으니 어쩔 수 없는거 아닌가 ??
❓❓❓❓❓❓❓ 왜 모르겠지..
input 에 대하여 우리가 예상한 예상값이 나오는 것을 검증하는 것은 당연한 것 아닌가?
그런데 위의 Assertions 처럼 작성하면, 내부적으로 Ratio 를 저런식으로 사용한다는 것(내부 구현)에 의존하게 되는 것 같다.. 그럼 책에서 말한 캡슐화는 대체 무슨 의미일까?? 잘못 작성한 것 같다. 제대로 작성하려면 어떻게 하는거지? 모르겠다 😥 ❓
💥 Mocking 을 사용할 때의 치명적(?) 단점 : 구현"방법"에 의존하는 테스트
우리가 테스트 코드를 작성하는 이유는 무엇일까????
왜 TDD 를 하라고 하는걸까?
개인적인 + 주워들어서 형성된 나의 생각으로는
- 코드를 먼저 작성하고 테스트 코드를 작성하다보면 "구현방법" 에 의존한 테스트 코드가 작성된다.
- 그런데 웃기게도 이제까지 나는 이렇게 작성된 테스트 코드가 있으면, 이제는 또, 이 테스트 코드가 깨지지 않도록 리팩토링을 해 나갔다는 것이다.
- 리팩토링이라는 것을 생각해보자. 리팩토링에서는 public 하게 제공하는 기능은 같되, 내부 구현을 좀 더 효율적이거나 클린한 코드로 변경하는 것일텐데.. 나는 내부 구현을 변경하지를 못하고 있게 되는 것이당.. why? 내부구현에 의존하여 작성된 테스트코드에 의존하여 리팩토링하려고 하고 있기 때문 😓
Mockito 의 when,return 이런 것을 사용하다보면, 클래스의 내부구현에 의존하는 테스트가 된다.
해당 클래스의 "기능(public 한 기능)" 을 변경하지 않는 한 , 테스트는 변경하지 않는 것이 가장 이상적인데, 이를 깨트리게 된다.
Fake 객체를 사용하면, Cash 가 제공하는 기능의 “내부 구현”이 바뀐다고 기존의 테스트가 깨지는 일이 발생하지 않을 수도 있다.
어떻게 이게 가능하다는 걸까?
먼저, Cash 가 제공하는 기능에는 변화가 없다! 단지 내부 구현만 변경이 일어나도록 한다.
Cash 의 in() 에서는 “현재 통화가 USD 인 경우라면 “ 이제 Exchange.rate(origin, target)가 아닌 Exchange(target) 메소드를 사용한다고 해 보자.
Exchange 인터페이스에 새로운 기능
float rate(String target); 을 추가한다
public CashV2 in(String currency) {
return new CashV2(
this.exchange,
this.cents * this.exchange.rate(currency)
);
}
우리의 Fake 클래스를 떠올려보자! 이는 Exchange 를 implement 하고 있다.
인터페이스에 새로운 추상 메소드가 추가되었다면, 인터페이스의 구현체인 Fake 에서는 이를 구현해줘야만 한다.
public interface ExchangeV2 {
float rate(String origin, String target);
float rate(String target);
final class Fake implements ExchangeV2 {
@Override
public float rate(String origin, String target) {
return this.rate("USD", target);
}
@Override
public float rate(String target) {
return 1.15f;
}
}
}
기존의 Fake 를 사용한 테스트 코드를 실행해 보자.
Mocking 을 사용하는 테스트코드와 달리 이 테스트는 깨지지 않는다.
결과적으로, 테스트 코드는 변경하지 않았다
그니까 , 모킹(Mocking)은 나빠요
모킹은 단위테스트를 위해 만들어지긴 했는데, 위에서 본 것 처럼 , 나쁜 프랙티스를 만들어낸다
- 테스트와, 단위테스트 대상 클래스의 내부구현을 결합시켜 → 내부 구현 변경시 → 유효하지 않은 구현과 결합되어 있는 테스트는 버려야하게됨
- 우리는, Cash 의 내부 결정(사적 영역) 에 대해 어떤 것도 가정할 권리가 없다!!!
- Cash가 “다른 클래스와 어떻게 상호작용" 하는지는 우리의 관심사가 아니다!!!!
책에서도 언급한 아주 따끔한 이야기
- 테스트에서 Exchange 인스턴스를 만들어서 주입해주다보니, Cash - Exchange 사이의 상호작용 방법에 대해 알 권리가 있다는 착각을 했었다.
ExchangeV2 exchange = new ExchangeV2.Fake(); CashV2 dollar = new CashV2(exchange, 500);
- 다시 한 번 !!! 우리는 객체의 구현방법을 알 권리가 없다!!!! 객체는 자율적인 객체다!! 우리는 객체의 구현방법에 의존적인 코드를 작성하여 그들의 변경에 대한 파급효과를 받아서도 안된다!! 라는 얘기를 하고 있다
Mocking 에서 제공하는.. verify
대부분 모킹 프레임워크에서는 “특정한 상호작용( A 객체와 B 객체 사이에 특정한 메소드 호출 발생 등)이 실제로 발생했는지 여부"와 “상호작용 횟수”를 “검증" 하는 기능을 제공한다.
A객체와 B 객체 사이에 특정한 메소드 호출이 발생했음을 테스트하기 위해 이를 사용하곤 했다..
매우 나빠요
이 역시 “ 그 특정한 상호작용 에 의존하는 단위테스트를” 만들어버린다.
- 객체와 의존대상의 상호작용 방식은 “ 캡슐화해야 하는 정보" → “숨겨야 하는 정보" 다
❓🙄 그렇다면 이제까지처럼, 객체 사이의 어떤 메소드가 일어났음을 테스트하는 것은 완전히 잘못된 걸까?🙄❓
그러니까, Fake 를 제공하는 인터페이스를 만드세요
내가 작성하는 인터페이스부터라도, Fake 객체를 만들어서 제공해주자
- 내 인터페이스의 “구현체"를 작성해 보는 것이 되기 때문에, “인터페이스의 사용자 관점에서의 고민" 도 할 수 있게 된다 !
만약
- 실제 DB 에 대한 I/O 를 해야하거나
- 실제 외부 서버API 와의 통신
을 해야하는 구현체를 가진 인터페이스(I)에 의존하는 A 클래스에 대한 단위테스트를 작성한다면
- 오랜시간이 걸리더라도 Fake 클래스를 작성해주자
- ex) 거대한 DB 대신, File 을 사용한 흉내를 낼 수도 있고
- ex) 실제 외부 서버 API 대신, XML 을 통한 흉내를 낼 수도 있다 ( 사실 이건 모르겠다.. )
'책 > 엘레강트 오브젝트' 카테고리의 다른 글
[elegant]ch04 ! 끝! (0) | 2022.06.23 |
---|---|
[elegant] ch03 (정리중) (0) | 2022.06.23 |
[elegant]ch02_ ~ 2.6.4 (0) | 2022.06.09 |
[elegant] 1. Birth 와 관련 자료 발표 영상 (0) | 2022.06.05 |