5 분 소요

이번 포스트에서는 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의 환경 구성

  1. Driver : 유닛 테스트를 위한 테스트 코드
  2. Stub : 유닛 테스트 시 실제 사용하기 어려운 다른 모듈을 가짜 구현으로 대체
  3. 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 typeequals() 메서드를 반드시 구현해야 올바른 비교가 가능하다.
    • 사용자 정의 클래스에서는 반드시 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 기반
  1. 테스트할 클래스를 만든다.
  2. Test Case Class를 만든다.
  • extends TestCase를 해야한다.
  1. 테스트 메서드를 작성한다.
  • 메서드 명을 testXXX 형식으로 사용한다. (JUnit 3 스타일)
  1. 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를 작성하고 그 테스트를 통과시키는 코드를 작성한다.

이유 / 효과

  1. 요구사항을 명확하게 정의하게 된다.
    • 테스트가 그 요구사항을 문서화한다.
  2. 테스트가 프로그램으로 작성되므로 자동적인 실행이 가능하다.
  3. 모든 테스트를 자동 실행하여 새로운 기능을 추가 시에 기존 기능이 깨졌는지 자동 확인이 가능하다.
    • 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 여부 확인 가능
테스트 시간 각 테스트 실행 시간 표시
  • 유용한 기능
  1. Fail 테스트만 보기가 가능하다.
    • 디버깅에 유용하다.
  2. 전체 테스트 반복 실행 용이하다.
    • Regression Test 효과를 확보한다.