본문 바로가기

[도서리뷰]

[도서리뷰] 테스트 주도 개발 시작하기

반응형

책 정보

저자 : 최범균

출판사 : 가메출판사

발행일 : 2020년 02월 18일

페이지 : 303


리뷰

테스트 코드도 코드이기 때문에 유지보수 대상이다

개발자라면 한번쯤 테스트 코드에 관심을 가지게 될거고 그러다보면 TDD 를 한번쯤 들어봤을거다.

TDD에서 강요하는 것은 하나다.

  1. 테스트코드를 먼저 작성한다.
  2. 구현코드를 작성한다.
  3. 리팩토링을 한다

결론: 위 3가지를 통해 빠르게 피드백을 하고 코드의 품질을 높인다.

제목에 써있듯이 이 책은 테스트코드를 처음 접하는 사람이라면 추천하고 싶은 책이다.

테스트코드가 처음이라면 사실 어떻게 시작해야하는지 막힐 수 있는데, 간단한 예제(2장 더하기 계산, 3장 암호확인)들을 책 초반에 소개해줌으로서 개념을 잡아주고 상세한 설명으로 넘어간다.

+ 마지막에 주는 팁들이 있는데 그 내용들도 되게 쏠쏠한 편이다.


내용

모든 내용을 다루기에는 너무 많으니 핵심이라 생각되는 것들만 가볍게 정리하자

  1. TDD란?
  2. 대역
  3. 테스트가 어려운 코드와 쉬운 코드

1. TDD란?

TDD가 뭔지 알기전에, 이전의 개발 방식은 어떤지 짚고 넘어가자.

  1. 기능에 대해 설계 고민 - 어떤 클래스와 인터페이스를 도출할지, 어떤 메소드를 넣을지 고민
  2. 위에서 고민한 걸 토대로 구현에 대한 고민을 시작 - 얼추 그림이 그려지면 개발을 시작
  3. 기능에 대한 구현을 완료한 것 같으면 기능 테스트
  4. 기능이 의도한 대로 동작하지 않거나 문제 발생시 디버깅

이 방식으로 개발을 하게되면 뭐가 문제인가?

  1. 디버깅을 위해 많은 코드를 탐색해야한다
  2. 코드를 작성한 개발자와 테스트하는 사람이 다를 수 있다.
  3. 테스트를 위한 환경을 구성하기 어렵다. (ex. 데이터베이스, WAS)

본론으로 돌아와서 그러면 TDD는 어떻게 개발을 하는가?

기능을 검증하는 테스트 코드를 먼저 작성하고 테스트를 통과시키기 위해 개발을 하는거다.

개발 방식

  1. 기능을 검증하는 테스트 코드 작성 → 컴파일 오류가 나는게 정상이다 겁먹지 마라
  2. 컴파일 오류를 없애기 위한 클래스, 인터페이스, 메서드 등을 작성
  3. 테스트 실행을 통해 실패하는 것을 확인
  4. 테스트를 통과할 만큼만 구현
  5. 리팩토링할 요소가 보이면 리팩토링

1~5번을 반복하면서 _점진적으로 기능을 완성_해 가는거다.

이 방법을 레드(실패)-그린(성공)-리팩터 라고 부른다.

그러면 어느 형태로 개발을 진행해야하는가?

  1. 쉬운 경우에서 어려운 경우로 진행
  2. 예외적인 경우에서 정상인 경우로 진행
    이렇게 하는 이유는 단순하다.
    TDD에서 중요시하는 것 중 하나가 "테스트 작성 → 통과시킬 만큼 구현 → 리팩토링" 이 과정을  빠르게 사이클을 돌리는건데 어려운 경우에서 시작을 하게 되면 구현해야 하는 코드가 너무 많아지는 문제가 있다.

2. 대역

테스트를 작성하다보면 DB, 타 API 서버와 같은 외부 요인이 필요한 시점이 있다. 그러나 외부 요인은 말 그대로 우리가 어떻게 컨트롤할 수 없을 뿐더러 테스트를 예측할 수 없게 만든다. 이럴 때 대역 (double) 을 사용하면된다. 

대역의 종류는 다음과 같이 있다

  1. 스텁 (stub) : 구현을 단순한 것으로 대체한다. 테스트에 맞게 원하는 동작을 수행
  2. 가짜 (fake) : 제품에는 적합하지 않으나 실제 동작하는 구현 제공
  3. 스파이 (spy) : 호출된 내역을 기록
  4. 모의 (mock) : 기대한 대로 상호작용하는지 행위를 검증한다. 

대역을 사용하게 되면 실제 구현이 없어도 실행 결과를 확인할 수 있는 장점이 크다.

아마 테스트를 작성하다 보면 모의 객체를 과하게 쓰는 자신을 보게 될텐데, 이는 오히려 검증하는 코드가 복잡해지고 테스트 대상과 모의 객체 간의 상호작용이 조금만 바뀌어도 테스트가 깨지기 어려워지는 문제가 발생한다.

디비와 같은 저장소에 연결하는 거라면 가짜 구현을 사용하는것을 고려해봐라. 더 간결해지고 쉬워진다

3. 테스트가 어려운 코드와 쉬운 코드

테스트를 어렵게 하는 요인에는 다음과 같은 경우가 있다

  1. 하드 코딩된 경로 - 윈도우와 맥만해도 두개의 기본 경로가 다르다
  2. 의존 객체를 직접 생성  
  3. 정적 메서드 사용 - 만약 타 서버와 통신하는 코드라면, 해당 서버와 연결하는 기능도 구현되어야한다.
  4. 실행 시점에 달라지는 결과 - LocalDate(), Random()
  5. 역할이 섞여 있는 코드 - 한 메서드에 역할이 여럿인 경우

그러면 어떻게 해야 쉬운 코드인가

  1. 하드 코딩된 상수를 생성자나 메서드 파라미터로 받기
  2. 의존 대상은 주입받자
  3. 테스트하고 싶은 코드 분리
  4. 시간이나 임의값 생성 기능 분리
  5. 외부 라이브러리는 직접 사용하지 말고 감싸서 사용하기

5번에 대해서 좀 설명을 하자면, 만약 외부에서 제공하는 라이브러리를 포함하고 있다면 해당 메서드는 대역으로 대처하기 어려울 수 있다. 이런 경우 외부 라이브러리를 감싸는 타입을 하나 만들어서 사용하면 테스트하기 편해진다.

fun login(id: String, password: String): LoginResult {
	val response = 0
    val authorized = AuthUtil.authorize(authKey)
    ...
}

# 다음처럼 한번 감싸자
class AuthService {
	private val authKey = "someKey"
    
    fun authenticate(id: String, password: String): Int {
    	val authorized: Boolean = AuthUtil.authorize(authKey)
        ...
    }
}

...
class LoginService(
	private val authService: AuthService
) {
	fun login(id: String, password: String): LoginResult {
		val response = 0
    	val authorized = authService.authenticate(id, password)
    	...
	}
}
728x90