uju's Tech

테스트하기 어려운 코드를 테스트하기 용이하게 개선하기 본문

Programming/Kotlin

테스트하기 어려운 코드를 테스트하기 용이하게 개선하기

ujusy 2023. 1. 31. 22:58

반년 넘게 ...  미뤄오던 포스팅 주제.. 드디어 작성합니다.....  :--(  

 


우리는 요구사항을 구현하고 우리가 기대한 대로 동작하는지 확인하기 위해 테스트를 작성한다.

 

🤔 그런데 내가 테스트하고 싶은 코드가 테스트하기 어렵다면 어떻게 테스트할 것인가?

 

테스트하기 어려운 코드는 데이터베이스나 외부 API 등 외부의 무엇인가에 의존하고 있는 코드일 수도, 랜덤한 값에 의존하는 동작이 있는 등 동작 자체를 예측하기 어려운 코드일 수도 있다.

 

예제를 통해 알아보자

주사위를 굴려 나온 눈의 숫자가 3 이하면 한 칸을 이동하고, 4 이상이면 두 칸을 이동한다는 요구사항이 있는 게임을 구현한다고 하자. 이를 간단하게 구현한다면 아래와 같은 코드를 떠올릴 수 있을 것이다.

 

 

그러면 이 코드를 테스트하려면 어떻게 해야할까? 아마 move 함수에 대해서 다음과 같은 두 가지 케이스를 테스트 해야할 것이다.

1. 주사위의 눈이 3 이하면 location은 1 증가해야 한다.
2. 주사위의 눈이 4 이상이면 location은 2 증가해야 한다.

그런데 move 함수 내부를 살펴보면 RandomDice 클래스의 value() 함수를 호출해 매 실행마다 랜덤한 숫자를 가져온다. 이 함수가 어떤 값을 반환할지 예측할 수 없으므로 테스트를 작성하기 어렵다.

 

테스트를 위해 value() 함수에 min 과 max 매개변수를 추가하는 사람은 없을 거라고 생각한다. 지금의 구현은 테스트를 작성하기 용이하지 않은 구조다. 이 코드를 테스트하기 용이한 구조로 개선해보자.

 

먼저 어떻게 하면 이 코드를 쉽게 테스트할 수 있을지 생각해보자. 만약 주사위를 굴렸을 때 나오는 값을 우리가 통제하여 항상 원하는 값이 나오도록 할 수 있다면 어떨까?

 

Step1. Dice 인터페이스 추가

Dice 인터페이스를 만들고 RandomDice가 이 인터페이스를 구현하도록 해보자.

 

 

 

RandomDice와 똑같이 생긴 인터페이스를 만든다고 뭐가 달라질까 하는 생각이 들 수도 있지만 계속 진행해보자.

Step2. 의존성 주입

Game 클래스가 RandomDice를 사용하는 대신 Dice 인터페이스를 사용하도록 하자. 그러면 RandomDice 대신 Dice 인터페이스를 구현하는 어떤 클래스가 오더라도 Game 클래스는 이 클래스를 사용할 수 있다.

그러면 Game 클래스가 Dice 인터페이스를 구현한 여러 클래스 중 어떤 것에 의존할지 어떻게 결정할 수 있을까? Game 클래스가 사용할 클래스를 외부에서 주입해주면 된다. 이것이 의존성 주입이다.

아래 코드와 같이 개선할 수 있다.

 

Step3. Test Double

이제 Game 클래스는 Dice 인터페이스를 구현하기만 한다면 어떤 클래스를 주입해 주더라도 그 역할을 다할 수 있게 되었다. 앞서 주사위를 굴렸을 때 나오는 값을 우리가 통제하여 항상 원하는 값이 나오도록 할 수 있다면 테스트가 쉬워질 수 있을 것이라고 했다. 이제는 그 꿈을 이룰 수 있다.

 

Dice 인터페이스를 구현하면서 우리가 원하는 값만을 반환하는 테스트용 주사위를 만들어 Game에 주입한다. 그러면 move() 함수 내에서 주사위를 굴릴 때마다 우리가 원하는 값이 반환될 것이다. 즉, 무작위로 나오는 값을 통제해서 우리가 테스트하고자 하는 상황을 만들어 테스트할 수 있게 된다.

 

 

DiceStub은 생성 시점에 diceNumber를 매개변수로 받아 주사위를 굴릴 때 마다 같은 값을 반환한다. 그리고 테스트를 할 때 이 DiceStub을 사용하면 된다. 여기서 Dice 인터페이스로 추상화한 부분이 빛이 난다.

그러면 테스트 코드는 아래와 같을 것이다.

 

 

이런 경우에 반드시 Stub만을 사용해야 하는 것은 아니다. 다양한 테스트 더블이 있고 일단 Dice 인터페이스를 구현하기만 한다면 어떤 테스트 더블을 사용하든 문제없다. 어떤 테스트 더블이 적합한지는 테스트 대상과 테스트 대상의 의존하는 객체가 어떤 행위를 하는지 등 다양한 요인에 따라 매 번 다르다. 이 예제를 다른 테스트 더블로도 구현해보아도 도움이 될 것이다.

 

** 참고 **

 

- 엘레강트 오브젝트 2.8장

 

- https://martinfowler.com/articles/mocksArentStubs.html#TheDifferenceBetweenMocksAndStubs

 

Mocks Aren't Stubs

Explaining the difference between Mock Objects and Stubs (together with other forms of Test Double). Also the difference between classical and mockist styles of unit testing.

martinfowler.com

'Programming > Kotlin' 카테고리의 다른 글

[kotest] Data Driven Testing (Parameterized Test)  (0) 2022.05.16
[kotest] Property-based Testing  (0) 2022.05.08
Comments