Testable Code by Jin-Wook Chung
이 글에서 별도 설명없이 사용되는 테스트라는 용어는 코드로 수행되는 자동화테스트를 의미한다.
코드로 수행되는 자동화테스트는 모든 시나리오에 적용될 수 없다. 테스트가 어렵거나 경우에 따라 불가능 경우도 있기 때문이다. 사용자가 비밀번호를 잃어버릴 경우에 대비하여, 비밀번호 재설정을 위한 메일을 발송하는 시나리오를 생각해보자. 메일이 잘 발송되었는지 테스트하는 것은 불가능하지 않지만 상당히 어렵다.이 글에서는 무엇이 테스트를 어렵게 만드는지 일반화된 사실로 정리해보고자 한다. 특정 기능을 수행하는 코드를 작성할 때, 같은 동작을 하더라도 테스트가 어려운 코드가 있는 반면, 테스트하기 쉬운 코드가 있다. 우리가 최대한 후자의 경우로 코드를 작성하려면 테스트하기 쉬운코드와 어려운 코드를 구분할 수 있어야 한다.
What does testable code mean?
이 글에서 테스트하기 어렵다(non-testable)는 말은 테스트가 불가능하다란 의미가 아니다. 테스트하기 쉬운코드(testable) 보다 테스트가 상대적으로 비용이 많이 든다는 의미이다. 이 비용을 기꺼이 지불하고 테스트를 수행한다고 해서 전혀 틀렸다는 의미가 아니란 것이다. 모든 코드가 테스트되면 좋지만, 앞서 이메일 발송기능과 같이 테스트 비용이 높아 테스트 하지 못하는 경우가 있다. 이 글에서는 비용을 고려하여 상대적인 의미로 테스트하기 쉽다 또는 어렵다라는 표현을 사용하고 있다. 다만 얼마나 테스트하기 어려운지 정량적으로 기술하지 못하는 것은 나의 역량 한계다.
불확실성(non-determinism)
테스트 대상 코드가 테스트가 가능하려면 같은 입력 값에 항상 같은 결과를 반환해야 한다. 아래 GetAMOrPM
메소드는 현재 시각에 따라 “AM” 또는 “PM” 문자열을 반환한다. 실행되는 시각에 따라 결과가 좌지우지 되므로 우리는 이 메소드의 결과 값에 확신을 가질 수 없다. 즉 테스트가 불가능하다. 불확실성은 테스트를 어렵게 만드는 첫 번째 요인이다. 랜덤수, 임의시각은 테스트를 어렵게 만드는 불확실성의 대표적인 예이다.
public string GetAMOrPM()
{
var now = DateTime.Now;
if (now.Hour < 12)
{
return "AM";
}
else
{
return "PM";
}
}
불확실성은 랜덤수, 임의시각 뿐 아니라 아래와 같은 다양한 경우에서 존재한다.
- 전역변수
- 로컬머신에 존재하는 파일
- 데이터베이스의 특정 레코드
- 웹서비스를 통해 응답받는 내용
전역변수에서 값을 읽을 때, 첫 번째 실행과 두 번째 실행의 결과 사이에 차이가 있을 수 있다. 그 사이에 누가 전역변수의 값을 바꿔버리 수 있기 때문이다. 로컬파일에 있는 내용을 읽어 오는 경우도 마찬가지이다. 누가 파일 내용을 바꿔버리거나 파일을 삭제해버릴 수 있다. 데이터베이스 또는 웹서비스에서 데이터를 읽어오는 경우도 이와 다르지 않다.
불확실성을 유발하는 모든 경우에 공통된 특징이 있다. 외부세상에서 제공하는 값에 의존한다는 것이다. 랜덤수, 임의시각도 외부에서 제공하는 값이라는 테두리에 포함시켜 이해할 수 있다.
부수효과(side effects)
테스트를 어렵게 만드는 두 번째 요인은 부수효과이다. 부수효과라 함은 관측가능한 값의 변경을 의미한다. 메일을 발송하거나, 파일에 어떤 내용을 쓰거나, 그리고 데이터베이스에 어떤 값을 기록하는 이 모든 경우가 부수효과이다.
부수효과는 외부세상에 변경을 가하지만 리턴 값을 가지지 않는 특징이 있다. 이 때문에 이를 검증하는 테스트는 비용이 많이 들게 되며 테스트하기 힘든 코드가 된다.
순수함수(pure functions)
테스트를 어렵게 만드는 불확실성은 외부세상에서 값을 읽어오는 것에 기인한다. 부수효과는 반대로 외부세상에 값을 기록하는 것에서 기인된다. 불확실성과 부수효과를 가지지 않는 즉, 외부세상에 단절된 상태를 함수형 프로그램에선 순수함수라 한다. 테스트하기 쉬운 코드와 순수함수는 같은 의미가 된다.
리턴타입별 테스트 용이성
메소드 또는 함수를 크게 둘로 반환타입이 있는 것과 없는 것(void
)으로 나눌 수 있다. 반환타입이 없는 경우는 외부세상을 변경시키는 것이다. 테스트하기 어려운 코드에 해당된다. 반환타입이 있는 메소드에 대해 테스트 용이성을 따지는 것은 조금 복잡하다. (imperative programming 에서) 반환타입있는 메소드일지라도 내부 코드에서 외부세상을 변경시키는 코드가 담겨 있을 수 있다. 그런 코드를 담고 있지 않다면 CQS(Command Query Separation)를 준수하는 것이다. 반환타입이 있고 외부세상을 변경시키는 코드가 없는 메소드는 CQS의 Query 메소드 범주에 속한다. Query 메소드가 테스트하기 쉬운가? 그럴 수도 있고 아닐 수도 있다. 그것은 Query 메소드가 외부세상에서 값을 읽어오느냐, 아니냐에 달려 있다. 데이터베이스에서 읽어온 데이터를 통해 값을 반환한다면 테스트하기 어렵다. 반면 외부세상과 단절된 상태에서 결과를 반환한다면 테스트하기 쉬운 경우다.
하스켈 언어에서는 외부세상과 소통을 IO
타입으로 나타낸다. IO
타입을 반환하는 함수는 비순수함수, 그렇지 않은 함수를 순수함수로 분류한다. 하스켈에서 테스트 용이성은 반환타입만으로 분류할 수 있는 셈이다. 외부세상과 소통(IO)은 대부분 작업이 완료될 때까지 기다림을 수반한다. 이 기다림 비용을 줄이기 위해 C# 같은 언어에서는 Task
타입을 통해 async await 기능을 제공한다. C#에서는 하스켈과 같이 정확히 순수/비순수함수로 양분할 순 없지만, 큰 틀에서 Task
를 반환하는 메소드를 테스트하기 어려운 경우로 분류할 수 있다.
Summary
테스트하기 쉬운 코드를 작성하려면 먼저 어떤 코드가 테스트하기 쉬운지 구분할 수 있어야 한다. 불확실성과 부수효과를 가지지 않는 즉, 외부세상과 단절된 상태의 코드를 테스트하기 쉬운코드라 한다. 테스트 용이성을 결정짓는 외부세상과 단절은 IO 작업와 관련된다. IO 작업을 하스켈에서는 IO
타입으로 C#에서는 Task
타입으로 표현한다. 이들 타입을 반환하는 함수, 메소드는 테스트하기 어려운 코드이다. 같은 기능을 수행하더라도 테스트하기 어려운 코드가 있는 반면, 그 반대의 경우도 있다. 이어지는 글에서는 어떻게 하면 테스트하기 어려운 코드는 줄이고, 테스트하기 쉬운 코드를 늘릴 수 있는지에 대해 알아보려 한다.
blog comments powered by Disqus