JUnit : 자바 단위 기반 테스트

2025. 3. 4. 15:39Java/Java 문법

[JUnit 기본 개념]

1. JUnit

  • JUnit은 Java에서 단위 테스트를 작성하고 실행할 수 있도록 도와주는 프레임워크
  • 단위 테스트 (Unit Test): 하나의 작은 기능이 기대한 대로 동작하는지 검증하는 테스트
  • 테스트 자동화 지원: JUnit은 메서드가 올바르게 동작하는지 자동으로 검증할 수 있다.  

 

2. JUnit을 사용하는 이유

"그냥 main() 메서드에서 System.out.println()으로 출력해서 확인하면 되지 않나?" 라는 생각이 들 수도 있지만, 테스트 자동화의 핵심은 테스트를 반복적으로 빠르고 정확하게 실행하는 것이다. 

2-1. System.out.println() 방식의 단점

public static void main(String[] args) {
    Calculator calculator = new Calculator();
    int result = calculator.sum(2, 3);
    System.out.println(result); // 5가 출력되면 "맞다"고 사람이 확인해야 함
}
  • 사람이 직접 확인해야 함 (자동화 ❌)
  • 예상한 값과 다르면 어디서 문제가 생겼는지 바로 알기 어려움
  • 수십 개의 메서드를 테스트할 때 매우 비효율적

2-2. JUnit을 사용한 단위 테스트의 장점

@Test
void testSum() {
    Calculator calculator = new Calculator();
    int result = calculator.sum(2, 3);
    assertEquals(5, result);  // 자동으로 검증해줌!
}
  • 실행하면 자동으로 검증됨 → 예상값과 실제값이 같으면 테스트 통과, 다르면 실패로 표시됨 
  • 여러 개의 테스트를 한 번에 실행 가능 
  • 개발 과정에서 테스트를 반복 실행하면서 코드가 잘 동작하는지 지속적으로 확인 가능 

2-3. 테스트 코드의 필요성 정리 

  • 테스트 자동화로 코드의 신뢰성을 높일 수 있음
    • 사람이 직접 테스트할 수도 있지만, 실수할 가능성이 높고 반복하기 번거롭다. 
    • Unit을 사용하면 테스트 자동화가 가능해서 코드가 잘 동작하는지 빠르게 확인할 수 있다. 
  •  유지보수할 때 실수를 줄일 수 있음
    • 기존 코드를 수정하면 의도치 않게 다른 기능이 망가질 수 있다.
    • 테스트 코드가 있으면 코드 수정 후에도 기존 기능이 잘 동작하는지 확인 가능 → 회귀 테스트(Regression Test)
  • 리팩토링(코드 개선)시 부담이 줄어든다
  • 협업 시 커뮤니케이션이 쉬워짐

 

3. 테스트 코드의 위치 

3-1. 테스트 코드의 위치 

my-project/
│── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/hnjee/section01/jupiter/Calculator.java  ← 💡 메인 코드!
│   ├── test/
│   │   ├── java/
│   │   │   └── com/hnjee/section01/jupiter/CalculatorTest.java  ← 💡 테스트 코드!
  • 일반적으로 테스트 코드는 메인 코드와 분리된 별도 디렉토리에서 관리한다.
    • 메인코드: src/main/java 아래
    • 테스트 코드: src/test/java 아래
  • 테스트 클래스는 메인 클래스와 같은 패키지 구조를 따라간다.
  • 테스트 클래스는 메인 클래스 이름 뒤에 Test를 붙인다.

3-2. 테스트 클래스의 위치를 실제 코드와 같은 패키지 구조로 맞추는 이유

src/main/java/com/hnjee/section01/jupiter/Calculator.java
src/test/java/com/hnjee/section01/jupiter/CalculatorTest.java
  • 프로젝트 구조가 깔끔하게 정리됨
  • 테스트할 클래스를 쉽게 찾고 관리할 수 있음
    • CalculatorTest가 바로 Calculator를 테스트하는 클래스라는 게 명확해짐 
  • 패키지 접근제한자 문제 없이 원활하게 테스트 가능
    • 접근제한자: public > protected > default(package-private) > private
    • 같은 패키지 구조를 유지하면 같은 패키지로 인식되므로 package-private 클래스도 테스트할 수 있음
    • 위의 예시에서 Calculator.java와 CalculatorTest.java는 같은 패키지 이므로
      Calculator가 package-private이더라도, CalculatorTest.java에서 Calculator 클래스 접근 가능해진다. 

3-3. 자바 패키지 개념 

  • 패키지는 디렉토리 구조가 아니라 논리적인 개념!
  • src/main/java와 src/test/java는 물리적으로는 다른 디렉토리지만,
    자바 패키지 개념에서는 같은 패키지 구조를 유지하면 같은 패키지로 인식됨
  • 예를 들어 위 예시의 경우 Calculator와 CalculatorTest는 실제 파일은 서로 다른 곳에 있다.
    하지만 com.hnjee.section01.jupiter 라는 패키지명은 동일하니까 자바에서는 같은 패키지로 인식한다. 

 

4. 테스트 클래스, 메서드

  • 하나의 클래스 → 하나의 테스트 클래스
    한 개의 기능 → 하나의 테스트 메서드를 만드는 방식이 권장된다. 
  • 테스트를 기능별로 나누는 이유!
    • 가독성 & 유지보수성 증가
      테스트가 실패하면 어느 기능에서 문제가 생겼는지 바로 알 수 있고 나중에 코드가 변경되더라도 필요한 부분만 테스트 가능
    • 독립적인 실행 가능
      여러 테스트를 한꺼번에 실행할 수도 있고, 특정 기능만 개별적으로 실행할 수도 있음.
      모든 테스트가 한 메서드에 있으면, 하나라도 실패하면 나머지 코드도 제대로 실행되지 않을 수 있음!
  • 테스트 클래스 이름은 "테스트하려는 클래스 + Test"
    테스트 메서드는 test로 시작하지 않아도 되지만, test로 시작하면 찾기 쉬워서 보통 많이 쓴다. 

 

5. JUnit 테스트 구조 예시 

메인 코드 파일 

//파일 위치: src/main/java/com/hnjee/section01/jupiter/Calculator.java
//패키지: com/hnjee/section01/jupiter
public class Calculator {
    public int sum(int a, int b) { return a + b; }
    public int subtract(int a, int b) { return a - b; }
    public int multiply(int a, int b) { return a * b; }
    public int divide(int a, int b) {
        if (b == 0) throw new IllegalArgumentException("0으로 나눌 수 없음");
        return a / b;
    }
}

 

테스트 코드 파일 

//파일 위치: src/test/java/com/hnjee/section01/jupiter/CalculatorTest.java
//패키지: com/hnjee/section01/jupiter
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    private final Calculator calculator = new Calculator();

    @Test
    void testSum() {
        assertEquals(5, calculator.sum(2, 3));
    }

    @Test
    void testSubtract() {
        assertEquals(1, calculator.subtract(3, 2));
    }

    @Test
    void testMultiply() {
        assertEquals(6, calculator.multiply(2, 3));
    }

    @Test
    void testDivide() {
        assertEquals(2, calculator.divide(6, 3));
        assertThrows(IllegalArgumentException.class, () -> calculator.divide(4, 0));
    }
}
  • 테스트 클래스는 src/test/java에 위치
  • 테스트 클래스를 실제 코드와 같은 패키지 구조로 맞춤
  • 테스트 클래스 이름: Calculator+Test = CalculatorTest
  • 각 메서드(testSum, testSubtract 등)는 하나의 기능만 테스트
  • 여러 개의 assertEquals()를 넣어 다양한 입력값을 테스트
  • 예외 상황 테스트 (assertThrows())도 포함 (0으로 나눌 때 예외 발생하는지 확인)

 


[JUnit5 사용법]

JUnit 구성요소

1. Annotation (어노테이션) : 테스트 메서드를 정의하고 설정하는 어노테이션

2. Assertion (어설션) : 테스트가 기대한 값과 실제 값이 같은지 확인하는 메서드

 

1. Annotation (어노테이션) 

1-1. 기본적인 어노테이션 

어노테이션 설명 
@Test 테스트 메서드를 나타낸다.
@DisplayName 테스트 클래스나 메서드의 이름을 지정한다.

1-2. 테스트 실행 전후 설정 어노테이션 

어노테이션 행 시점 설명 
@BeforeEach 각 테스트 메서드 실행 전 객체 초기화 같은 작업
@AfterEach 각 테스트 메서드 실행 후 리소스 정리 같은 작업
@BeforeAll 테스트 클래스 실행 전 (한 번만 실행) static 메서드 필요
@AfterAll 테스트 클래스 실행 후 (한 번만 실행) static 메서드 필요

1-3. 테스트 실행 제어 어노테이션

어노테이션 설명 
@Disabled 특정 테스트를 비활성화(건너뛰기)
@Tag 특정 그룹의 테스트만 실행하고 싶을 때
@RepeatedTest(int count) 동일한 테스트를 여러 번 반복 실행할 때

 

 

2. Assertion (어설션) 

2-1. Assertion 메소드 

검증 메서드 설명 
assertEquals(expected, actual) 기대한 값과 실제 값이 같은지 검증
assertNotEquals(expected, actual) 기대한 값과 실제 값이 다른지 검증
assertTrue(condition) 조건이 true인지 검증
assertFalse(condition) 조건이 false인지 검증
assertNull(value) 값이 null인지 검증
assertNotNull(value) 값이 null이 아닌지 검증
assertThrows(Exception.class, () -> 실행코드) 예외가 발생하는지 검증

2-2. AssertJ 라이브러리 

  • AssertJ는 더 강력한 Assertion(검증) 기능을 제공하는 라이브러리
  • JUnit Assertions과 AssertJ 차이 
//기본 JUnit Assertions 사용 
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class JUnitAssertionTest {
    @Test
    void testWithJUnit() {
        String result = "Hello, World!";
        
        assertEquals("Hello, World!", result);
        assertTrue(result.startsWith("Hello"));
    }
}
//AssertJ 사용 예시
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;

class AssertJTest {
    @Test
    void testWithAssertJ() {
        String result = "Hello, World!";

        assertThat(result)
            .isEqualTo("Hello, World!")
            .startsWith("Hello")
            .endsWith("World!");
    }
}
  • AssertJ의 장점
    • JUnit이 제공하지 않는 다양한 검증 메서드 제공 (컬렉션, 리스트, JSON 비교 등)
    • 체이닝(Chaining) 지원 → 한 줄로 여러 개의 검증을 깔끔하게 작성 가능, 가독성 향상
    • 에러 메시지 자동 생성 → 실패 시 더 직관적인 오류 메시지 제공
  • AssertJ에서 자주 쓰는 기능
    • 기본 검증: isEqualTo(), isNotNull(), contains() 등
    • 숫자 비교: isGreaterThan(), isBetween() 등
    • 리스트 검증: contains(), hasSize(), doesNotContain() 등
    • 예외 검증: assertThatThrownBy()
    • 객체 필드 값 검증: hasFieldOrPropertyWithValue()
    • Boolean 검증: isTrue(), isFalse()

[테스트 주도 개발 TDD와 JUnit]

1. 테스트 주도 개발(TDD)

  • 테스트를 먼저 작성하고, 그 테스트를 통과하기 위한 코드를 작성하는 개발 방법론
  • TDD의 장점은 코드의 품질을 높이고, 버그를 조기에 발견하며, 유지보수를 용이하게 만든다는 점이다.

2. TDD의 3단계 (Red-Green-Refactor)

단계 설명
🔴 Red
실패하는 테스트 작성
먼저 테스트를 작성하지만, 해당 기능이 아직 구현되지 않았으므로 테스트가 실패한다.
🟢 Green
테스트를 통과하는 코드 작성
테스트를 통과할 수 있도록 최소한의 코드를 작성한다.
🟡 Refactor
코드 개선
중복 코드 제거, 성능 개선 등을 통해 코드를 리팩토링한다.

 

3. JUnit 과 TDD

  • JUnit은 자바 개발자에게 필수적인 도구로, 단위 테스트를 통해 코드의 안정성과 품질을 보장한다.
  • 다양한 기능과 유연한 구조를 갖춘 JUnit은 테스트 주도 개발(TDD)와 같은 현대적인 소프트웨어 개발 방법론을 실천하는 데 큰 도움을 준다.
  • 또한, AssertJ와 같은 라이브러리를 함께 사용하면 테스트 코드의 가독성과 유연성을 더욱 높일 수 있다.