Ver2.5 야, 너두 테스트 코드 짤 수 있어! JUnit으로 개발자 레벨업 🚀

야, 너두 테스트 코드 짤 수 있어! JUnit으로 개발자 레벨업 🚀
개발의 퀄리티를 수직 상승시키는 비밀 병기, JUnit.
코딩의 자신감을 풀충전하고 칼퇴를 부르는 단위 테스트의 세계로 초대할게!
프롤로그: "아니, 테스트 코드를 왜 짜요?"
안녕! 코딩의 세계에 발을 들인 우리 동료 개발자 친구들. 👋
매일같이 새로운 기능을 만들고, 버그를 잡고, 리팩토링의 늪에서 허우적대느라 고생이 많아.
그런데 말이야, 혹시 이런 생각해 본 적 없어?
"내가 지금 고친 이 코드 때문에... 다른 멀쩡한 기능이 죽으면 어떡하지?"
"새로운 기능을 추가했는데, 기존 로직이랑 꼬여서 대참사가 나면...?"
"밤새 만든 내 코드가 사실은 거대한 버그 덩어리라면...?"
이런 불안감, 우리 개발자들에겐 거의 뭐 직업병 같은 거잖아. 😅
이 불안감을 해소하고, 내 코드에 대한 확신과 자신감을 갖게 해주는 마법 같은 도구가 바로 **'테스트 코드'**야. 그리고 자바 세상에서 그 마법을 가장 잘 부리는 도구가 바로 **JUnit**이지.
🤔 "테스트 코드? 그거 그냥 기능 잘 돌아가는지 손으로 확인해보면 되는 거 아니에요?"
물론 그것도 테스트지. 하지만 그걸 '수동 테스트'라고 불러. 매번 코드를 수정할 때마다 모든 기능을 일일이 손으로 클릭하고, 값을 입력하고, 결과를 확인하는 과정... 생각만 해도 끔찍하지 않아? 😱
JUnit을 이용한 **'자동화된 단위 테스트'**는 이 모든 과정을 코드로 작성해서, 단 몇 초 만에 수백, 수천 개의 테스트를 실행하고 결과를 알려줘. 이건 마치 내 코드를 24시간 지켜주는 든든한 보디가드를 두는 것과 같아.
이 글에서는 JUnit이 대체 뭐고, 왜 써야 하는지, 그리고 어떻게 써야 내 개발 라이프가 윤택해지는지 A부터 Z까지 친한 친구가 알려주듯 쉽고 재미있게 파헤쳐 볼 거야. 이 글을 다 읽고 나면, 더 이상 테스트 코드 작성을 두려워하지 않고, 오히려 즐기게 될 거라고 확신해. 자, 그럼 시작해볼까?
JUnit 5, 너 정체가 뭐야? (feat. 단위 테스트)
JUnit을 제대로 알려면, 먼저 '단위 테스트(Unit Test)'가 뭔지부터 알아야 해.
어렵게 생각할 거 없어. 말 그대로 **'단위(Unit)'**를 테스트하는 거야.
여기서 '단위'는 뭘까?
보통 프로그래밍에서 의미 있는 가장 작은 단위를 말해. 자바에서는 보통 **메서드(Method)** 하나, 또는 잘 만들어진 **클래스(Class)** 하나가 그 단위가 될 수 있지.
즉, 단위 테스트는 '내가 만든 특정 메서드가 의도한 대로 정확하게 동작하는지'를 검증하는 코드야. 예를 들어, 두 숫자를 더하는 `add(a, b)` 메서드를 만들었다면, `add(2, 3)`을 호출했을 때 정말로 `5`가 반환되는지를 확인하는 코드를 짜는 거지.
이런 단위 테스트를 쉽고 체계적으로 작성할 수 있도록 도와주는 여러 도구와 규칙의 모음을 **'테스트 프레임워크(Test Framework)'**라고 부르고, 자바 진영의 부동의 원탑, 표준이 바로 **JUnit**인 거야.
JUnit의 역사: JUnit 4에서 JUnit 5로의 진화
JUnit도 벌써 버전 5까지 오면서 많은 발전을 거듭했어.
과거의 표준이었던 JUnit 4는 거대한 단일 라이브러리(monolithic) 구조였어. 모든 기능이 `junit.jar` 파일 하나에 몽땅 들어있었지. 쓰기엔 편했지만, 새로운 기능을 추가하거나 자바의 새로운 변화(예: 람다)를 발 빠르게 적용하기엔 좀 무거운 구조였어.
그래서 등장한 게 바로 **JUnit 5**!
JUnit 5는 '모듈화(Modular)'라는 혁신적인 변화를 들고 나왔어. 거대한 통짜 라이브러리가 아니라, 여러 개의 작은 모듈로 분리해서 필요한 것만 쏙쏙 골라 쓸 수 있게 된 거지.
JUnit 5는 크게 3가지 모듈로 구성돼.
- JUnit Platform: 테스트를 실행시켜주는 런처(Launcher) 역할을 해. IDE(인텔리제이, 이클립스)나 빌드 도구(메이븐, 그래들)가 이 Platform을 통해 테스트를 발견하고 실행하는 거지. 일종의 '무대'라고 생각하면 돼.
- JUnit Jupiter: 우리가 앞으로 배울 JUnit 5의 새로운 프로그래밍 모델이야. `@Test`, `@DisplayName` 같은 새로운 어노테이션과 강력한 기능들이 모두 여기에 포함돼 있어. '무대 위의 주인공'이지.
- JUnit Vintage: 과거의 유산, 즉 JUnit 3, 4 버전으로 짜인 테스트 코드를 JUnit 5 환경에서 그대로 실행할 수 있게 해주는 '하위 호환성' 모듈이야. '특별 출연한 원로 배우'랄까?
이런 구조 덕분에 우리는 이제 JUnit Jupiter라는 최신 엔진을 사용하면서도, 필요하다면 과거의 코드나 다른 테스트 프레임워크까지 유연하게 통합할 수 있게 된 거야. 완전 멋지지 않아? 😎
자, 이제 진짜 시작: JUnit 5 첫걸음 떼기
백문이 불여일견! 이론은 이쯤 하고, 직접 코드를 짜면서 감을 익혀보자.
가장 먼저 할 일은 우리 프로젝트에 JUnit 5를 모셔오는 것. 즉, 의존성을 추가하는 일이야.
1. 프로젝트에 JUnit 5 날개 달아주기 (의존성 설정)
요즘은 대부분 메이븐(Maven)이나 그래들(Gradle) 같은 빌드 관리 도구를 사용하니까, 이걸 기준으로 설명할게.
💡 잠깐! Spring Boot를 쓴다면?만약 네가 스프링 부트 프로젝트를 `spring-boot-starter-test` 의존성을 포함해서 생성했다면, 축하해! 넌 아무것도 할 필요가 없어. 스프링 부트가 이미 JUnit 5를 포함한 필수 테스트 라이브러리들을 알아서 다 챙겨주거든. 역시 '근본'은 달라.
Gradle 사용 시 (build.gradle)
가장 많이 쓰는 그래들 설정이야. `build.gradle` 파일의 `dependencies` 블록에 아래 내용을 추가해줘.
dependencies {
// JUnit 5 (Jupiter) API 및 Engine
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
// (선택) 파라미터화된 테스트를 위한 의존성
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2'
}
// build.gradle 파일 최상단에 아래 내용 추가
test {
useJUnitPlatform()
}
Maven 사용 시 (pom.xml)
메이븐을 쓴다면 `pom.xml` 파일의 `
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
자, 이제 우리 프로젝트는 JUnit 5를 사용할 모든 준비를 마쳤어!
2. 나의 첫 테스트 코드 작성하기
세상에서 제일 간단한 `Calculator` 클래스를 만들고, 이 녀석이 덧셈을 잘하는지 테스트해보자.
Step 1: 테스트할 대상 코드 작성 (Calculator.java)
`src/main/java` 아래에 간단한 계산기 클래스를 하나 만들자.
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
Step 2: 테스트 코드 작성 (CalculatorTest.java)
이제 이 `Calculator`를 테스트할 코드를 짤 차례야.
테스트 코드는 보통 `src/test/java` 디렉토리에 만들어. 실제 배포될 코드와 분리하기 위해서지.
클래스 이름은 보통 `[테스트할 클래스명]Test` 라고 짓는 게 국룰이야. `Calculator`를 테스트할 거니까 `CalculatorTest`가 되겠지?
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
@DisplayName("두 숫자의 덧셈이 정확한지 테스트한다")
void add_test() {
// given (준비)
Calculator calculator = new Calculator();
int a = 5;
int b = 3;
// when (실행)
int result = calculator.add(a, b);
// then (검증)
assertEquals(8, result, "5 + 3의 결과는 8이어야 합니다.");
}
}
와! 드디어 첫 테스트 코드를 완성했어. 🎉
코드가 좀 낯설지? 하나씩 뜯어보자.
@Test: "이 메서드는 테스트 메서드입니다!" 라고 JUnit에게 알려주는 가장 중요한 어노테이션이야. 이게 없으면 그냥 평범한 메서드일 뿐, JUnit이 실행해주지 않아.@DisplayName("..."): 테스트의 목적을 사람이 읽기 좋은 문장으로 설명해주는 어노테이션이야. 이걸 쓰면 나중에 테스트 결과를 볼 때 "add_test" 같은 메서드 이름 대신 "두 숫자의 덧셈이 정확한지 테스트한다" 라고 예쁘게 보여줘. 가독성이 엄청나게 올라가겠지? 강력 추천!assertEquals(expected, actual, message): 이게 바로 **'검증(Assertion)'** 메서드야. "기대하는 값(expected)과 실제 결과값(actual)이 같은지 확인해줘!" 라는 뜻이지. 만약 두 값이 다르면 테스트는 실패하고, 세 번째 인자인 메시지를 보여줘.
3. 테스트의 황금률: Given-When-Then (혹은 Arrange-Act-Assert)
위 예제 코드의 주석을 봤어? `given`, `when`, `then`.
이건 테스트 코드를 작성하는 아주 유명하고 좋은 패턴이야. 이 구조를 따르면 테스트의 의도가 명확해지고, 다른 사람이 읽기도 훨씬 편해져.
- **Given (준비, Arrange):** 테스트를 실행하기 위해 필요한 모든 상태를 준비하는 단계야. 객체를 생성하고, 필요한 데이터를 세팅하는 등의 작업을 해.
- **When (실행, Act):** 실제로 테스트하고 싶은 동작, 즉 메서드를 호출하는 단계야.
- **Then (검증, Assert):** `When` 단계의 결과가 우리가 기대한 대로 나왔는지 검증하는 단계야. `assertEquals` 같은 검증 메서드를 사용해서 확인하지.
이 패턴은 꼭 지켜야 하는 강제 규칙은 아니지만, 좋은 테스트 코드를 만들기 위한 최고의 가이드라인이니까 꼭 기억해두자!
4. JUnit의 핵심, Assertions 파헤치기
`assertEquals` 말고도 JUnit은 다양한 상황에 쓸 수 있는 유용한 검증 메서드들을 잔뜩 제공해. 몇 가지만 더 알아볼까?
import static org.junit.jupiter.api.Assertions.*;
// ... 테스트 클래스 내부 ...
@Test
@DisplayName("다양한 검증 메서드 사용 예제")
void assertion_examples() {
String name = "Jaenung";
String nullName = null;
boolean isFun = true;
// assertTrue / assertFalse: 조건이 참/거짓인지 검증
assertTrue(isFun, "코딩은 재미있어야 합니다!");
assertFalse(5 < 3, "5는 3보다 작지 않습니다.");
// assertNull / assertNotNull: 객체가 null인지 아닌지 검증
assertNull(nullName, "nullName 변수는 null이어야 합니다.");
assertNotNull(name, "name 변수는 null이 아니어야 합니다.");
// assertThrows: 특정 예외가 발생하는지 검증
// 아래 코드는 "10 / 0" 연산을 할 때 ArithmeticException이 발생하는지를 테스트
assertThrows(ArithmeticException.class, () -> {
int result = 10 / 0;
});
// assertAll: 여러 검증을 한 번에 실행
// 중간에 하나가 실패해도 멈추지 않고 모두 실행한 뒤 결과를 알려줌
assertAll("person",
() -> assertEquals("Kim", "Kim"),
() -> assertEquals(25, 25)
);
}
특히 `assertThrows`는 예외 상황을 테스트할 때 정말 유용하고, `assertAll`은 여러 속성을 가진 객체를 검증할 때 아주 편리해. 이 정도만 알아도 웬만한 단위 테스트는 문제없이 작성할 수 있을 거야!
JUnit 마스터 클래스: 더 강력한 기능들
기본기는 다졌으니, 이제부터는 JUnit을 한 단계 더 깊게 파고들어 보자.
지금부터 소개할 기능들을 잘 활용하면 테스트 코드의 퀄리티와 효율이 수직 상승할 거야.
1. 테스트 라이프사이클 관리: `@BeforeAll`, `@AfterAll`, `@BeforeEach`, `@AfterEach`
여러 테스트 메서드를 실행하다 보면, 공통적으로 필요한 준비(setup)나 마무리(teardown) 작업이 생기기 마련이야. 예를 들어, 모든 테스트 전에 데이터베이스 연결을 설정하거나, 각 테스트가 끝날 때마다 임시 파일을 삭제하는 것처럼 말이지.
JUnit은 이런 공통 작업을 위한 멋진 어노테이션들을 제공해.
import org.junit.jupiter.api.*;
class LifecycleTest {
@BeforeAll
static void beforeAllTests() {
System.out.println("모든 테스트 시작 전, 딱 한 번만 실행됩니다.");
// 예: DB 커넥션 풀 초기화, 무거운 설정 파일 로딩 등
}
@AfterAll
static void afterAllTests() {
System.out.println("모든 테스트가 끝난 후, 딱 한 번만 실행됩니다.");
// 예: DB 커넥션 풀 자원 해제
}
@BeforeEach
void beforeEachTest() {
System.out.println("==> 각 테스트 메서드 시작 전에 실행됩니다.");
// 예: 테스트에 사용할 객체 초기화
}
@AfterEach
void afterEachTest() {
System.out.println("==> 각 테스트 메서드 끝난 후에 실행됩니다.");
// 예: 테스트 데이터 정리
}
@Test
void test1() {
System.out.println("테스트 1 실행");
}
@Test
void test2() {
System.out.println("테스트 2 실행");
}
}
위 코드를 실행하면 콘솔에 찍히는 순서는 이럴 거야.
- 모든 테스트 시작 전, 딱 한 번만 실행됩니다.
- ==> 각 테스트 메서드 시작 전에 실행됩니다.
- 테스트 1 실행
- ==> 각 테스트 메서드 끝난 후에 실행됩니다.
- ==> 각 테스트 메서드 시작 전에 실행됩니다.
- 테스트 2 실행
- ==> 각 테스트 메서드 끝난 후에 실행됩니다.
- 모든 테스트가 끝난 후, 딱 한 번만 실행됩니다.
차이가 명확히 보이지?
`@BeforeEach` / `@AfterEach`는 **각각의 `@Test` 메서드마다** 실행돼서, 테스트 간의 독립성을 보장하는 데 아주 중요해. 한 테스트가 다른 테스트에 영향을 주면 안 되니까!
`@BeforeAll` / `@AfterAll`은 **테스트 클래스 전체에서 딱 한 번** 실행돼. 그래서 `static` 메서드여야만 해. 여러 테스트가 공유하는 무거운 자원을 준비하고 해제할 때 유용하지.
2. 테스트 그룹핑과 구조화: `@Nested`와 `@Tag`
테스트 케이스가 많아지면 관리하기가 힘들어져. 이럴 때 테스트를 논리적인 그룹으로 묶어주면 가독성과 구조가 훨씬 좋아져.
`@Nested`로 계층 구조 만들기
`@Nested`는 테스트 클래스 안에 내부 클래스(inner class)를 만들어서, 관련된 테스트들을 하나의 그룹으로 묶어주는 기능이야. 특정 시나리오나 상태에 따른 테스트를 그룹핑할 때 빛을 발하지.
@DisplayName("사용자 관리 기능 테스트")
class UserManagementTest {
@Nested
@DisplayName("사용자 생성 시나리오")
class CreateUserScenario {
@Test
@DisplayName("필수 정보가 모두 있으면 성공적으로 생성된다")
void createUser_with_valid_info() { /* ... */ }
@Test
@DisplayName("이메일이 중복되면 예외가 발생한다")
void createUser_with_duplicate_email() { /* ... */ }
}
@Nested
@DisplayName("사용자 조회 시나리오")
class ReadUserScenario {
@Test
@DisplayName("존재하는 ID로 조회하면 사용자 정보를 반환한다")
void findUser_with_existing_id() { /* ... */ }
@Test
@DisplayName("존재하지 않는 ID로 조회하면 예외가 발생한다")
void findUser_with_non_existing_id() { /* ... */ }
}
}
어때? `@Nested`를 쓰니까 '사용자 생성'과 '사용자 조회'라는 큰 맥락 안에서 세부 테스트들이 착착 정리되는 느낌이지? IDE에서도 계층 구조로 예쁘게 보여줘서 훨씬 보기 좋아.
`@Tag`로 테스트에 꼬리표 달기
`@Tag`는 테스트에 임의의 꼬리표(태그)를 붙이는 기능이야. 이걸 이용하면 특정 태그가 붙은 테스트만 골라서 실행할 수 있어.
class TaggingTest {
@Test
@Tag("fast")
@DisplayName("빠르게 실행되는 단위 테스트")
void fast_unit_test() { /* ... */ }
@Test
@Tag("slow")
@DisplayName("DB 연동이 필요한 느린 통합 테스트")
void slow_integration_test() { /* ... */ }
}
이렇게 태그를 붙여두면, 빌드 스크립트나 IDE 설정에서 "fast 태그가 붙은 테스트만 실행해줘!" 또는 "slow 태그는 빼고 실행해줘!" 같은 설정이 가능해져.
예를 들어, 개발 중에는 `fast` 테스트만 빠르게 돌려보고, CI/CD 파이프라인에서는 모든 테스트(`fast`, `slow`)를 실행하는 식으로 효율적인 테스트 전략을 짤 수 있지.
3. 테스트 코드의 혁명: `@ParameterizedTest` (파라미터화 테스트)
이건 JUnit 5의 킬러 기능 중 하나야. 정말 정말 중요하고 강력해!
만약 `isPositive(int number)` 라는 메서드를 테스트한다고 생각해봐. 1, 10, 100 등 여러 양수를 넣어서 테스트해봐야겠지? 이걸 `@Test`로 각각 만들면 코드가 중복되고 지저분해져.
이럴 때 `@ParameterizedTest`를 쓰면, 여러 다른 입력값(파라미터)을 가지고 **하나의 테스트 메서드를 여러 번 실행**할 수 있어.
`@ValueSource`: 가장 간단한 파라미터 소스
기본 타입(int, long, double, String 등)의 값들을 직접 제공할 때 사용해.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NumberValidatorTest {
@ParameterizedTest
@ValueSource(ints = {1, 5, 100, Integer.MAX_VALUE})
@DisplayName("양수 판별 테스트")
void isPositive_with_positive_numbers(int number) {
assertTrue(number > 0);
}
}
이 코드는 `isPositive_with_positive_numbers` 메서드를 총 4번 실행해. `number` 파라미터에 각각 1, 5, 100, `Integer.MAX_VALUE`를 넣어서 말이야. 코드가 엄청나게 깔끔해졌지?
`@CsvSource`: CSV 포맷으로 여러 인자 전달하기
입력값과 기대값을 함께 전달하는 등, 여러 개의 인자가 필요할 때 아주 유용해.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@ParameterizedTest
@CsvSource({
"1, 2, 3",
"5, 5, 10",
"-1, 1, 0",
"0, 0, 0"
})
@DisplayName("다양한 덧셈 케이스 테스트")
void parameterized_add_test(int a, int b, int expected) {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.add(a, b));
}
}
CSV(Comma-Separated Values) 문자열 하나가 테스트 케이스 하나가 되는 거야. `"1, 2, 3"`은 `a=1, b=2, expected=3`으로 매핑돼서 테스트를 실행해. 가독성도 좋고, 테스트 케이스를 추가하기도 정말 편해.
💡 더 많은 소스들!JUnit 5는 이 외에도 `@EnumSource`(Enum 상수를 파라미터로), `@CsvFileSource`(CSV 파일을 읽어서 파라미터로), `@MethodSource`(특정 메서드의 반환값을 파라미터로) 등 다양한 소스를 제공해. 상황에 맞게 골라 쓰면 테스트 코드의 재사용성과 효율성을 극대화할 수 있어.
실전! JUnit과 환상의 짝꿍, Mockito
지금까지는 `Calculator`처럼 외부 의존성이 없는 아주 간단한 객체만 테스트했어.
하지만 실제 세상의 코드는 그렇게 단순하지 않지. 대부분의 객체는 다른 객체와 협력해서 동작해. 예를 들어, `UserService`는 `UserRepository`를 이용해서 DB에 접근하고, `OrderService`는 `PaymentGateway`를 통해 결제를 요청하지.
이런 상황에서 `UserService`를 **'단위 테스트'**하고 싶다면 어떻게 해야 할까?
진짜 `UserRepository`를 사용하면 DB까지 연동해야 하잖아. 그럼 이건 더 이상 순수한 단위 테스트가 아니라 DB까지 엮이는 '통합 테스트'가 되어버려. 속도도 느려지고, DB 상태에 따라 테스트 결과가 달라질 수도 있지.
우리가 원하는 건 오직 `UserService`의 로직만을 고립시켜서 테스트하는 거야. 이때 필요한 게 바로 **'테스트 더블(Test Double)'**이고, 그중 가장 유명한 것이 **'목(Mock)'** 객체야.
가짜 객체 만들기: Mockito의 등장
**Mockito**는 자바에서 가장 인기 있는 목(Mock) 프레임워크야.
이 녀석을 사용하면, 실제 객체인 척 연기하는 '가짜 객체'를 아주 쉽게 만들 수 있어.
예를 들어, `UserRepository`의 가짜 객체(Mock)를 만들고, "만약 `findById(1L)` 메서드가 호출되면, 가짜 `User` 객체를 반환해줘!" 라고 미리 행동을 정의(stubbing)해 둘 수 있는 거지.
1. Mockito 의존성 추가
먼저 Mockito를 우리 프로젝트로 데려오자. (스프링 부트 `spring-boot-starter-test`에는 이미 포함되어 있어!)
Gradle (build.gradle)
dependencies {
// ... 기존 JUnit 의존성 ...
testImplementation 'org.mockito:mockito-core:4.5.1'
testImplementation 'org.mockito:mockito-junit-jupiter:4.5.1' // JUnit 5 연동을 위해
}
2. Mockito를 이용한 테스트 예제
간단한 `MemberService`와 `MemberRepository`를 예로 들어보자.
테스트 대상 코드
// Member.java
public class Member {
private Long id;
private String name;
// ... 생성자, getter ...
}
// MemberRepository.java (인터페이스)
public interface MemberRepository {
Optional<Member> findById(Long id);
void save(Member member);
}
// MemberService.java (우리가 테스트할 클래스)
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Optional<Member> findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
자, 이제 `MemberService`의 `findMember` 메서드를 테스트할 거야. 이때 실제 DB와 연동된 `MemberRepository` 구현체 대신, Mockito로 만든 가짜 `MemberRepository`를 사용할 거야.
Mockito를 사용한 테스트 코드
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) // JUnit 5에서 Mockito를 사용하겠다는 선언
class MemberServiceTest {
@Mock // 가짜(Mock) 객체 생성
private MemberRepository memberRepository;
@InjectMocks // @Mock으로 만든 객체를 이 객체에 주입
private MemberService memberService;
@Test
@DisplayName("존재하는 회원 ID로 조회 시 회원 정보를 반환한다")
void findMember_with_existing_id() {
// given (준비)
Long memberId = 1L;
Member fakeMember = new Member(memberId, "Test User");
// memberRepository.findById(1L)이 호출되면,
// 미리 만들어둔 fakeMember를 담은 Optional을 반환하도록 행동을 정의(stubbing)
when(memberRepository.findById(memberId)).thenReturn(Optional.of(fakeMember));
// when (실행)
Optional<Member> foundMember = memberService.findMember(memberId);
// then (검증)
assertTrue(foundMember.isPresent());
assertEquals("Test User", foundMember.get().getName());
}
}
코드가 조금 복잡해졌지만, 핵심은 이거야.
@ExtendWith(MockitoExtension.class): JUnit 5에게 "나 Mockito 기능 쓸 거니까 준비 좀 해줘!" 라고 알려주는 거야.@Mock: `MemberRepository` 타입의 가짜 객체를 만들어줘!@InjectMocks: `MemberService` 객체를 만들 때, 생성자에 `@Mock`으로 만든 `memberRepository` 가짜 객체를 자동으로 주입해줘!when( ... ).thenReturn( ... ): Mockito의 핵심! **"만약 ~가 호출되면, ~를 반환해라"** 라고 가짜 객체의 행동을 정의하는 부분이야. 위 예제에서는 `memberRepository.findById(1L)`이 호출되면 `Optional.of(fakeMember)`를 반환하도록 설정했지.
이렇게 하면, 우리는 DB 연결 없이도 `MemberService`가 `MemberRepository`로부터 받은 데이터를 잘 처리하는지, 그 로직 자체에만 집중해서 테스트할 수 있게 돼. 이게 바로 단위 테스트의 정수라고 할 수 있지.
이처럼 JUnit은 테스트의 구조와 실행을 담당하고, Mockito는 테스트 대상의 의존성을 가짜로 대체하는 역할을 하면서 환상의 시너지를 내. 실무에서는 이 두 가지를 함께 사용하는 게 거의 표준이야.
에필로그: 테스트 코드는 사랑입니다 ❤️
자, 지금까지 JUnit의 세계를 함께 여행해 봤어. 어때? 생각보다 어렵지 않지?
처음에는 테스트 코드를 작성하는 게 귀찮고, 오히려 개발 시간을 뺏는다고 느껴질 수도 있어. 나도 그랬으니까.
하지만 장담할게. 테스트 코드를 작성하는 습관을 들이는 순간, 너의 개발 라이프는 180도 달라질 거야.
- 더 이상 코드를 수정할 때마다 불안에 떨지 않아도 돼. 잘 짜인 테스트 스위트가 너의 든든한 안전망이 되어줄 테니까.
- 과감한 리팩토링이 가능해져. 테스트가 통과하는 한, 내부 구조를 어떻게 바꾸든 기능은 동일하게 동작한다는 확신을 가질 수 있어.
- 테스트 코드는 그 자체로 가장 정확하고 살아있는 문서가 돼. 새로운 동료가 프로젝트에 합류했을 때, 테스트 코드만 봐도 클래스의 역할과 기능을 빠르게 파악할 수 있지.
- 결과적으로, 버그는 줄어들고 개발 속도는 오히려 빨라져. 디버깅에 쏟던 시간을 새로운 가치를 만드는 데 쓸 수 있게 되는 거야.
물론, 좋은 테스트를 작성하는 건 또 다른 차원의 기술이야. 어떤 것을 테스트해야 하는지, 어떻게 테스트해야 좋은지 고민하는 과정이 필요하지. 하지만 오늘 배운 JUnit의 기능들만 잘 활용해도 충분히 훌륭한 시작을 할 수 있어.
혹시 알아? 이렇게 테스트 코드 작성 능력을 갈고닦다 보면, 너의 뛰어난 개발 역량을 다른 사람들에게 알려줄 기회가 생길지도 몰라. 예를 들어 **재능넷** 같은 플랫폼에서 너의 테스트 코드 작성 노하우나 클린 코드 컨설팅 재능을 공유할 수도 있겠지. 개발 실력은 나눌수록 더 커지는 법이니까!
오늘 당장 너의 작은 프로젝트에, 혹은 지금 만들고 있는 기능에 간단한 `@Test` 메서드 하나를 추가해보는 건 어떨까?
그 작은 시작이 너를 더 나은 개발자, 자신감 넘치는 개발자로 만들어 줄 첫걸음이 될 거야.
Happy Coding & Happy Testing! 🚀
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.