Test-Driven Development(TDD)
이번 포스트에서는 Test-Driven Development 에 대해 다루며 Unit Testing (JUnit test)에 집중적으로 다룬다.
Unit Test & TDD
Unit Testing
- 테스트되지 않은 코드는 올바르지 않다.
- 실무에서는 테스트가 필수적이다.
- 테스트 직무 수요도 존재한다.
- Divide-and-Conquer 접근
- 시스템을 작은 단위 (Unit) 으로 나누어 개별적으로 디버깅
- 특정 유닛에서만 버그를 추적-> 버그 발생 범위가 좁아진다.
- 전체 시스템 디버깅에 비해 효율적이다.
Test Suite
- 전체 시스템을 테스트하기 위한 포괄적인 테스트의 집합
- 언제든 반복 실행이 가능하고 자동화가 가능하다.
Ad hoc testing : 순간 떠오른 대로 테스트를 진행한다.
Test Suite : 계획된 전체 테스트로 신뢰도를 높인다.
장점 : 테스트가 디버깅 시간을 줄여주므로 결과적으로 개발 시간이 단축된다.
- 버그 감소, 유지보수 용이, 실제 서비스에서 큰 효과
단점 : 구현 비용 (코딩량이 더 증가한다.)
Regression Testing
- 회귀 테스트
- 기존에 잘 작동하던 기능이 변경 후에도 올바르게 작동하는지 확인하는 테스트
- 소프트웨어 기능 추가 / 수정 / 패치 시에 필요하다.
- 안정적이고 유지보수가 가능한 코드 베이스를 확보하고 안정성을 크게 향상 시킬 수 있다.
Unit Test의 환경 구성
- Driver : 유닛 테스트를 위한 테스트 코드
- Stub : 유닛 테스트 시 실제 사용하기 어려운 다른 모듈을 가짜 구현으로 대체
- Unit : 테스트 대상 (클래스, 함수, 모듈)
외부 의존성을 최소화, 대상 유닛만 집중적인 테스트가 가능하다.
큰 시스템을 작은 단위로 테스트하여 개발 속도가 향상한다.
JUnit
- JUnit이란 Java에서 Unit Test를 작성하고 실행하기 위한 프레임워크이다.
- Test suite 구성을 지원하여 자동화된 반복 테스트가 가능하다.
- Annotation 기반 문법으로 간편하게 테스트 작성
- 효과
- Regression Testing 자동화 기능으로 유지보수 비용이 적어진다.
- Test Suite를 정기적으로 실행한다.
- CI/CD 파이프라인 (지속적 통합)에 자연스럽게 통합이 가능하다.
Conventional Test 에는 어떤식으로 했을까?
public class IMathTestNoJUnit {
public static void main(String[] args) {
printTestResult(0);
printTestResult(1);
printTestResult(2);
...
}
private static void printTestResult(int arg) {
System.out.print("isqrt(" + arg + ") ==> ");
System.out.println(IMath.isqrt(arg));
}
}
기존의 테스트는 이런식으로 진행했다.
문제점
- 자동화된 검증이 불가능하다.
- 가능은 하지만 사람이 수동으로 결과를 해석해야 한다.
- 실패 여부가 자동 감지가 안된다.
- 결과가 많아질수록 분석하기에 매우 불편하다.
JUnit을 사용했을 때
public void testIsqrt() {
assertEquals(0, IMath.isqrt(0)); // 기대값과 실제값 자동 비교
assertEquals(1, IMath.isqrt(1));
...
}
- 자동으로 pass/fail 결과를 확인할 수 있다.
- 사람이 출력문을 해석할 필요가 없다.
- Failure 발생시에 자동으로 상세 원인을 알려준다.
- 자동화된 Regression Test가 가능하다.
Assertion Methods
| 메서드 | 설명 |
|---|---|
| assertEquals(a, b) | a와 b가 같은 값인지 확인 |
| assertTrue(a) | a가 true인지 확인 |
| assertFalse(a) | a가 false인지 확인 |
| assertNull(a) | a가 null인지 확인 |
| assertSame(a, b) | a와 b가 동일한 객체인지 확인 (같은 인스턴스) |
| assertNotSame(a, b) | a와 b가 서로 다른 객체인지 확인 |
- Assertion 메서드는 테스트의 검증 단계에서 핵심으로 사용된다.
- 실패 시 자동으로 AssertionFailedError가 발생한다.
- JUnit이 이를 잡아서 결과를 표시해준다.
equals 사용 시 주의사항
- Primitive type은
==연산자로 비교가 가능하다. - Object type은
equals()메서드를 반드시 구현해야 올바른 비교가 가능하다.- 사용자 정의 클래스에서는 반드시 equals() 재정의가 필요
- JUnit assertEquals에서는 내부적으로 equals() 를 사용한다.
JUnit Key Notions
- JUnit 기본 용어 정의
| 용어 | 설명 |
|---|---|
| Tested Class | 테스트 대상 클래스 |
| Tested Method | 테스트 대상 메서드 |
| Test Case | 특정 조건에서 클래스 메서드를 테스트 |
| Test Case Class | Test Case들을 담는 클래스 (테스트 클래스) |
| Test Case Method | Test Case Class 내부의 테스트 메서드 |
| Test Suite | 여러 Test Case를 모아 한 번에 실행 가능하게 만든 집합 |
핵심 포인트
- JUnit 에서는 Test Case Class안에 어려 개의 Test Case Method가 존재한다.
- Test Suite를 구성하여 여러 케이스를 자동 실행이 가능하다.
- 이는 Regression Test에 매우 효과적이다.
Test Class 만드는 과정
- JUnit 3 기반
- 테스트할 클래스를 만든다.
- Test Case Class를 만든다.
- extends TestCase를 해야한다.
- 테스트 메서드를 작성한다.
- 메서드 명을 testXXX 형식으로 사용한다. (JUnit 3 스타일)
- assert 메서드 사용으로 검증한다.
public class TestFailure extends TestCase {
public void testSquareRootException() {
try {
SquareRoot.sqrt(-4);
fail("Should raise an exception");
} catch (Exception success) { ... }
}
}
JUnit 3에서는 테스트 메서드명에 반드시 test 접두어가 필요했다.
- JUnit 4 기반
JUnit 4 부터는 @Test annotation을 사용하였다.
따로 test 접두어 사용 X
@Test public void squareRootTest1() {
int z = SquareRoot.sqrt(4);
assertEquals(2, z);
}
- Annotiation 기반으로 하여 더 현대적 사용이 가능했다.
- 메서드명도 자유롭게 작성이 가능했다.
Fixtures
JUnit 3 버전에서
- 각 테스트 실행 전후에 반드시 실행해야 하는 코드
-
준비 / 정리 코드
- 목적
- 테스트 사전 준비 또는 후처리가 필요한 경우에 사용한다.
- 예 : 객체 초기화, DB 연결, 환경 변수 설정
- 실행 흐름
runTest() -> setUp() -> run the test -> tearDown()-
각 Test Case 마다 반복실행 된다.
JUnit 4부터 (개선된 방식)
@Before protected void initialize() {
System.out.println("Before testing");
}
- JUnit 4 에서는 setUp() 을 대신해서 @Before annotation을 사용했다.
- @Before 가 붙은 메서드는 각 Test Case 실행 전에 자동 호출된다.
여러 개의 @Before 메서드를 사용 가능하다.
TearDown 개념 : 테스트가 끝난 이후에 정리 작업 이 필요할 때 사용했다.
예시) 메모리 해제, 파일 닫기, 플래그 초기화 등
@After protected void disposeObjects() {
System.out.println("After testing");
System.gc();
}
- 마찬가지로 test 실행 이후에는 @After annotation을 사용했다.
- 각 Test Case 실행 이후에 자동으로 호출된다.
- @After는 여러 개 선언이 가능하다.
Testing Exceptions
먼저 코드로 Junit 3, Junit 4를 보겠다.
- JUnit 3 방식
public void testDivisionByZero() {
try {
int n = 2 / 0;
fail("Divided by zero!"); // 예외가 발생하지 않으면 실패
} catch (ArithmeticException success) {
assertNotNull(success.getMessage());
}
}
- JUnit 4 방식
@Test(expected = ArithmeticException.class)
public void divideByZero() {
int n = 2 / 0;
}
- expected 속성으로 발생 예상 예외를 명시가 가능해졌다.
- try-catch문 없이 한 줄로 예외 테스트 처리가 가능해졌다.
Timed Test
- JUnit 4에서 지원한다.
- 테스트가 지정된 시간 안에 완료되지 않으면 실패를 처리한다.
@Test(timeout = 500) // 500ms 초과 시 실패
public void retrieveAllElementsInDocument() {
doc.query("//*");
}
- 성능 테스트는 항상 JUnit 이 완벽하게 해결해주는 것은 아니다.
- 너무 작은 timeout 설정은 환경 변화에 따라서 false negative 발생 가능성이 있다.
Test Method Structure
| 항목 | 설명 |
|---|---|
| 반환값 없음 | test 메서드는 void 반환 |
| 성공 시 | 아무것도 하지 않음 (pass 처리) |
| 실패 시 | AssertionFailedError 발생 |
| JUnit 역할 | 해당 오류를 자동으로 잡아줌 (직접 처리할 필요 없음) |
TDD
-
Test-first Development == TDD (Test-Driven Development)
-
개발을 시작할 때 먼저 Test를 작성하고 그 테스트를 통과시키는 코드를 작성한다.
이유 / 효과
- 요구사항을 명확하게 정의하게 된다.
- 테스트가 그 요구사항을 문서화한다.
- 테스트가 프로그램으로 작성되므로 자동적인 실행이 가능하다.
- 모든 테스트를 자동 실행하여 새로운 기능을 추가 시에 기존 기능이 깨졌는지 자동 확인이 가능하다.
- Regression Test 효과
JUnit 테스트는 과한 것인가?
- Extreme Programming (XP) 원칙 : 테스트가 안된 코드는 믿을 수가 없다.
- 실제 프로그램에서 이런 단순 클래스는 거의 없으므로 걱정할 필요는 없다.
- getter 메서드에 대해서는 굳이 테스트 안하는 경우도 있다.
Advanced Fixture
- 고급 Fixture도 존재한다.
- 큰 비용이 드는 설정 시에 사용한다.
@BeforeClass
public static void setUpClass() throws Exception {
// one-time initialization code
}
@AfterClass
public static void tearDownClass() throws Exception {
// one-time cleanup code
}
@BeforeClass, @AfterClass : 클래스 단위에서 한 번만 실행이 가능하다.
일반 @Before, @After 는 각 Test Case 실행마다 호출된다.
Viewing Results
Junit의 실행 결과 창
| 상태 표시 | 의미 |
|---|---|
| Green bar | 모든 테스트 통과 |
| Red bar | 테스트 실패 발생 |
| 개별 테스트 | Pass/Fail 여부 확인 가능 |
| 테스트 시간 | 각 테스트 실행 시간 표시 |
- 유용한 기능
- Fail 테스트만 보기가 가능하다.
- 디버깅에 유용하다.
- 전체 테스트 반복 실행 용이하다.
- Regression Test 효과를 확보한다.