[Book - JUnit IN ACTION 3판] 1. JUnit 시작하기
처음으로 TDD 방법론에 대해 학습하기 위해 현재 시점에서 가장 최근에 나왔고 deep하게 많은 내용을 담을 수 있을 것 같아서
JUnit IN ACTION 3판을 읽어보려고 한다.
마틴 파울러(Martin Fowler) : 소프트퉤어 개발에서 이토록 짧은 코드가 이토록 많은 사람에게 이토록 큰 도움을 준 적은 없었다.
소개
모든 코드는 테스트가 필요하다.
하지만 작은 테스트 묶음을 수동으로, 반복적으로 실행하는 것은 시간이 늘어날수록 단순 노동이 된다고 생각한다.
따라서 테스트 자동화를 해야 된다.
나는 지금까지 테스트 코드에 대해서 잘 몰랐지만 앞으로는 자동화된 테스트를 만드는 것이 개발 과정에 반드시 포함되어야 한다고 생각한다.
전체적인 테스트를 통과해야 해당 컴포넌트가 제대로 동작한다는 것을 증명할 수 있기 때문이다.
테스트 코드에서 기본은 단위 테스트라고 생각한다.
단위 테스트에 대한 일반적인 설명은 ‘예상되는 입력 범위 내에서의 각 입력에 대해 메서드가 예상되는 값을 반환하는지 확인한다’ 정도로 이해할 수 있다.
이는 인터페이스를 통해 메서드의 동작을 테스트하는 것으로 귀결된다.
단위 테스트(Unit Test)
개별적인 작업 단위의 동작을 검사하는 테스트로, 작업 단위는 다른 작업의 완료에 직접적으로 의존하지 않는다.
JUnit 없이 테스트하기
JUnit 없이 더하기 테스트를 해보자.
1
2
3
4
5
public class Calculator {
public double add(double num1, double num2) {
return num1 + num2;
}
}
위와 같이 코드를 작성하면 컴파일하는 데 문제는 없지만 런타임에도 문제가 없는지 알 수 없어서 재차 확인해봐야 한다.
단위 테스트의 핵심 원칙은 “프로그램 기능에는 자동화된 테스트가 수반되어야 한다”이다.
즉, add 메서드는 계산기의 주요 기능을 구현한 것인데 해당 구현이 제대로 작동하는지 증명할 수 있는 자동화된 테스트가 누락된 것이다.
프로그램이 제대로 동작한다는 확신을 가지려면 개발자 대부분은 아무리 간단한 기능이라도 자동화된 테스트가 있어야 한다.
자동화된 기능 테스트나 인수 테스트가 있어야 프로그램 기능이 잘 동작한다는 사실을 증명할 수 있기 때문이다.
나중에 테스트 프로그램을 확장하기 쉽게 계산기의 기능을 모듈식으로 설계해서 예외 처리를 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CalculatorTest {
private int nbErrors = 0;
public void testAdd() {
Calculator calculator = new Calculator();
double result = calculator.add(10, 20);
if (result != 30) {
throw new IllegalStateException("Bad result: " + result);
}
}
public static void main(String[] args) {
CalculatorTest test = new CalculatorTest();
try {
test.testAdd();
} catch (Throwable e) {
test.nbErrors++;
e.printStackTrace();
}
if (test.nbErrors > 0) {
throw new IllegalStateException("There were " + test.nbErrors + " error(s)");
}
}
}
애플리케이션이 복잡해지고 테스트가 애플리케이션에 점점 개입하게 되면,
위 코드처럼 사용자 정의 테스트 프레임워크를 계속 만들어 나가고 보수하는 일이 개발자에게 부담이 될 수 있다.
위 코드에서 try-catch 문으로 단위 테스트를 했다. 하지만 실무에서 사용할 수준의 단위 테스트는 아니고,
너무 큰 범위의 try-catch 문은 유지 보수 시 문제가 된다.
프로그램에 어떤 테스트를 실행해야 하는지 어떻게 알 수 있을까?
테스트를 등록하는 메서드를 만들면 실행이 되어야 하는 테스트의 리스트를 알려 줄 수 있지 않을까?
이 과정에서 JAVA의 Reflection를 추가적으로 구현해야 한다.
Reflection 이란
힙 영역에 로드 된 Class 타입의 객체를 통해, 접근 제어자 상관없이 원하는 클래스의 정보에 접근해서 조작할 수 있도록 지원하는 API이다.
- 필드, 메서드, 인터페이스 목록 가져오기
- 상위 클래스 가져오기
- 애노테이션 가져오기
- 생성자 가져오기
- …
다행히 위와 같은 문제는 JUnit 팀이 해결 방안을 제시했다.
테스트마다 각각의 클래스 인스턴스와 클래스 로더 인스턴스를 사용하고, 테스트마다 각각의 오류를 보고하게 만들어 놨다.
JUnit 팀은 이 프레임워크를 설계할 때 세 가지 구체적인 목표를 정의했다.
- 프레임워크는 유용한 테스트를 작성하는 데 도움이 되어야 한다.
- 프레임워크는 시간이 지나도 그 가치를 유지하는 테스트를 만드는 데 도움이 되어야 한다.
- 프레임워크는 코드를 재사용하여 테스트를 작성하는 비용을 낮추는 데 도움이 되어야 한다.
단위 테스트 프레임워크 이해하기
단위 테스트에는 프레임워크가 따라야 하는 세 가지 best practice가 있다.
- 단위 테스트는 다른 단위 테스트와 독립적으로 실행되어야 한다.
- 프레임워크는 각 단위 테스트의 오류를 파악하여 알려 주어야 한다.
- 어떤 테스트를 실행할지 쉽게 정의할 수 있어야 한다.
위 규칙 외에 테스트를 쉽게 추가할 수 있어댜 한다는 점도 눈여겨보아야 할 중요한 원칙이다.
JUnit을 활용하여 테스트하기
1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calculator = new Calculator();
double result = calculator.add(10, 20);
assertEquals(30, result, 0);
}
}
- CalculatorTest 테스트 클래스를 정의한다. (클래스명에서 Test라는 접미사를 붙이는 것이 테스트를 작성하는 일반적인 관행이다.)
- @Test 애노테이션을 달아 해당 메서드가 단위 테스트 메서드임을 나타낸다.
- 정적으로 가져온 assertEquals() 메서드를 사용해서 테스트 결과를 확인한다.
assertEquals(30, result, 0)
- 계산기의 더하기 결과인
result
를 예상 값 30과 비교한다. - 예상 값이 실제와 같이 않으면 JUnit은 unchecked exception을 던지고 검사는 실패한다.
세 번째 인자는 부동 소수점 계산을 포함하여 오차가 생길 수 있는 계산에 자주 쓰인다.
이 인자로 범위 비교를 할 수 있는데, result가 (30 - 0)와 (30 + 0) 범위 내에 있으면 테스트가 통과된다.
이 인자를 사용하는 assertEquals 메서드는 반올림이나 절사 오류가 있을 수 있는 수학적 계산을 하거나 파일 수정 날짜를 비교할 때 유용하다.
- 계산기의 더하기 결과인
JUnit을 활용하기 전과 후를 비교해 보자.
확실히 JUnit을 활용하는 것이 Test를 작성하기 쉽다.
또한 자동으로 테스트를 실행할 수 있다.
정리
JUnit으로 자동화된 단위 테스트를 사용함으로써 새로운 코드가 잘 작동하고 기존 테스트가 실패하지 않는지를 반복해서 확인할 수 있다.