엘레강트 오브젝트의 ch2.8 을 읽으며 실습의 필요성이 느껴져 아래와 같이 진행해보았다.

생각과정이 꽤 많이 들어가서 이상한 코드들이 존재할 수 있으니 많은 첨언 부탁드립니다 🥺


외부 자원( 외부 서버 등)에 의존하는 클래스를 테스트하기 위한 방법은 뭐가 있을까??

  • 의존하는 인터페이스에 대한 모의객체 생성하기
    • 우리는 이 가짜 객체 중 Mocking 의 단점에 대해 살펴볼 것이다
    • 우리는 이 가짜 객체 중 하나인 Fake 객체를 사용 해 볼 것이다.
      • 우리는 이 가짜 객체 중 Fake 객체의 장점에 대해 살펴볼 것이다

결론은 → 가능하다면 인터페이스에 대한 Fake 클래스를 작성하여 단위테스트를 합시다!!!
(이 글을 작성할 때는 test double 에 대한 정리가 안되어 있었어서 그런지 혼란스러운 부분이 좀 많은 듯 하다. Test dobule 에 대한 개념도 정리해 두는게 좋을 것 같다 여기엔 안 쓸거임)

  • 테스트 클래스에 대하여 Mocking 을 사용하면 안되는 이유 간단 정리 -> 단위테스트 대상 클래스의 내부구현에 의존적인 테스트를 작성하게 된다. 리팩토링 과정에서 테스트 코드 역시 변경해야 한다.

1.1. Mocking 을 사용할 때 단점 1 : 테스트 코드가 장황해짐

좌 : Mocking 을 사용

우 : Fake 객체를 사용 

 

Mocking 과 Fake 객체를 사용하여 Cash 클래스에 대한 단위테스트를 작성해 보자 ! 

1.1.1. (추가) 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 를 저런식으로 사용한다는 것(내부 구현)에 의존하게 되는 것 같다.. 그럼 책에서 말한 캡슐화는 대체 무슨 의미일까?? 잘못 작성한 것 같다. 제대로 작성하려면 어떻게 하는거지? 모르겠다 😥 ❓

1.2. 💥 Mocking 을 사용할 때의 치명적(?) 단점 :  구현"방법"에 의존하는 테스트 

우리가 테스트 코드를 작성하는 이유는 무엇일까????

왜 TDD 를 하라고 하는걸까?

개인적인 + 주워들어서 형성된 나의 생각으로는

  • 코드를 먼저 작성하고 테스트 코드를 작성하다보면 "구현방법" 에 의존한 테스트 코드가 작성된다.
  • 그런데 웃기게도 이제까지 나는 이렇게 작성된 테스트 코드가 있으면, 이제는 또, 이 테스트 코드가 깨지지 않도록 리팩토링을 해 나갔다는 것이다.
  • 리팩토링이라는 것을 생각해보자. 리팩토링에서는 public 하게 제공하는 기능은 같되, 내부 구현을 좀 더 효율적이거나 클린한 코드로 변경하는 것일텐데.. 나는 내부 구현을 변경하지를 못하고 있게 되는 것이당.. why? 내부구현에 의존하여 작성된 테스트코드에 의존하여 리팩토링하려고 하고 있기 때문 😓

 

Mockito 의 when,return 이런 것을 사용하다보면, 클래스의 내부구현에 의존하는 테스트가 된다. 

해당 클래스의 "기능(public 한 기능)" 을 변경하지 않는 한 , 테스트는 변경하지 않는 것이 가장 이상적인데, 이를 깨트리게 된다. 

 

1.3. 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 을 사용하는 테스트코드와 달리 이 테스트는 깨지지 않는다.

결과적으로, 테스트 코드는 변경하지 않았다

 


2. 그니까 , 모킹(Mocking)은 나빠요

모킹은 단위테스트를 위해 만들어지긴 했는데, 위에서 본 것 처럼 , 나쁜 프랙티스를 만들어낸다

  • 테스트와, 단위테스트 대상 클래스의 내부구현을 결합시켜 → 내부 구현 변경시 → 유효하지 않은 구현과 결합되어 있는 테스트는 버려야하게됨
  • 우리는, Cash 의 내부 결정(사적 영역) 에 대해 어떤 것도 가정할 권리가 없다!!!
    • Cash가 “다른 클래스와 어떻게 상호작용" 하는지는 우리의 관심사가 아니다!!!!

책에서도 언급한 아주 따끔한 이야기

  • 테스트에서 Exchange 인스턴스를 만들어서 주입해주다보니, Cash - Exchange 사이의 상호작용 방법에 대해 알 권리가 있다는 착각을 했었다.
ExchangeV2 exchange = new ExchangeV2.Fake();

CashV2 dollar = new CashV2(exchange, 500);
  • 다시 한 번 !!! 우리는 객체의 구현방법을 알 권리가 없다!!!! 객체는 자율적인 객체다!! 우리는 객체의 구현방법에 의존적인 코드를 작성하여 그들의 변경에 대한 파급효과를 받아서도 안된다!! 라는 얘기를 하고 있다

 


2.1. Mocking 에서 제공하는.. verify

대부분 모킹 프레임워크에서는 “특정한 상호작용( A 객체와 B 객체 사이에 특정한 메소드 호출 발생 등)이 실제로 발생했는지 여부"와 “상호작용 횟수”를 “검증" 하는 기능을 제공한다.

 

A객체와 B 객체 사이에 특정한 메소드 호출이 발생했음을 테스트하기 위해 이를 사용하곤 했다.. 

매우 나빠요

이 역시 “ 그 특정한 상호작용 에 의존하는 단위테스트를” 만들어버린다.

  • 객체와 의존대상의 상호작용 방식은 “ 캡슐화해야 하는 정보" → “숨겨야 하는 정보" 다

❓🙄 그렇다면 이제까지처럼, 객체 사이의 어떤 메소드가 일어났음을 테스트하는 것은 완전히 잘못된 걸까?🙄❓


3. 그러니까, 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
복사했습니다!