JUnit : 자바 단위 기반 테스트
2025. 3. 4. 15:39ㆍJava/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와 같은 라이브러리를 함께 사용하면 테스트 코드의 가독성과 유연성을 더욱 높일 수 있다.