Spring Boot를 처음 공부할 때는 보통 브라우저로 직접 확인합니다.
예를 들어 회원을 등록하려면 주소창에 이런 식으로 입력해볼 수 있습니다.
http://localhost:8080/members/new?name=kim
그리고 회원 목록을 확인하려면 다시 아래 주소로 접속합니다.
http://localhost:8080/members
이 방식도 처음에는 괜찮습니다.
하지만 기능이 많아지면 매번 브라우저로 직접 확인하는 방식은 불편해집니다.
그래서 개발자는 테스트 코드를 작성합니다.
이번 글에서는 JUnit을 이용해 테스트 코드를 처음 작성해보겠습니다.
이번 글의 목표는 딱 하나입니다.
실행 버튼 누르는 것 말고 코드로 확인하기
테스트 코드란?
테스트 코드는 내가 만든 코드가 의도한 대로 동작하는지 확인하는 코드입니다.
쉽게 말하면, 사람이 직접 클릭하고 확인하는 일을 코드가 대신 해주는 것입니다.
예를 들어 회원 등록 기능이 있다고 해보겠습니다.
우리가 확인하고 싶은 것은 이런 것입니다.
이름이 kim인 회원을 저장하면
회원 목록에 kim이 들어 있어야 한다.
이걸 매번 브라우저로 확인할 수도 있지만, 테스트 코드로 작성해두면 버튼 한 번으로 확인할 수 있습니다.
왜 테스트 코드가 필요할까?
처음에는 이런 생각이 들 수 있습니다.
"그냥 실행해서 확인하면 되는 거 아닌가?"
작은 예제에서는 맞습니다.
하지만 기능이 늘어나면 직접 확인하는 방식은 금방 한계가 옵니다.
예를 들어 기능이 10개라면 매번 10개를 손으로 확인해야 합니다.
코드를 수정할 때마다 다시 확인해야 합니다.
이 과정은 시간이 오래 걸리고, 실수하기도 쉽습니다.
테스트 코드를 작성해두면 이런 장점이 있습니다.
- 기능이 제대로 동작하는지 빠르게 확인할 수 있음
- 코드를 수정한 뒤 기존 기능이 망가지지 않았는지 확인할 수 있음
- 내가 의도한 동작을 코드로 기록할 수 있음
- 나중에 다시 봐도 기능의 규칙을 이해하기 쉬움
즉, 테스트 코드는 단순히 귀찮은 추가 작업이 아니라 코드를 안전하게 바꾸기 위한 장치입니다.
JUnit이란?
JUnit은 Java에서 테스트 코드를 작성할 때 많이 사용하는 도구입니다.
Spring Boot 프로젝트를 만들 때 기본으로 테스트 관련 설정이 들어 있는 경우가 많습니다.
보통 테스트 코드는 아래 폴더에 작성합니다.
src/test/java
실제 기능 코드는 보통 아래에 작성합니다.
src/main/java
즉, 역할을 나누면 이렇게 볼 수 있습니다.
| 위치 | 역할 |
|---|---|
src/main/java | 실제 애플리케이션 코드 |
src/test/java | 테스트 코드 |
처음에는 이렇게 기억하면 됩니다.
main은 실제 코드,
test는 확인용 코드
테스트할 대상 다시 보기
이전 글에서 회원 등록 예제를 만들었습니다.
그중 Service 코드는 대략 이런 형태였습니다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member join(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
}
return memberRepository.save(name);
}
public List<Member> findMembers() {
return memberRepository.findAll();
}
}
여기서 테스트하고 싶은 것은 두 가지입니다.
- 이름을 넣으면 회원이 저장되는지
- 이름이 비어 있으면 에러가 나는지
이번 글에서는 이 두 가지를 테스트 코드로 확인해보겠습니다.
테스트 파일 만들기
아래 위치에 테스트 파일을 만듭니다.
src/test/java/com/example/demo/MemberServiceTest.java
파일 이름은 보통 테스트할 클래스 이름 뒤에 Test를 붙입니다.
MemberService → MemberServiceTest
이렇게 하면 어떤 클래스를 테스트하는 코드인지 한눈에 알기 쉽습니다.
첫 번째 테스트 코드
아래 코드를 작성합니다.
package com.example.demo;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MemberServiceTest {
@Test
void 회원가입을_하면_회원목록에_저장된다() {
// given
MemberRepository memberRepository = new MemberRepository();
MemberService memberService = new MemberService(memberRepository);
// when
memberService.join("kim");
List<Member> members = memberService.findMembers();
// then
assertEquals(1, members.size());
assertEquals("kim", members.get(0).getName());
}
}
@Test는 무엇일까?
@Test
void 회원가입을_하면_회원목록에_저장된다() {
}
@Test는 이 메서드가 테스트 메서드라는 표시입니다.
JUnit은 @Test가 붙은 메서드를 찾아서 실행합니다.
즉, Spring Boot 애플리케이션을 브라우저로 실행하는 것이 아니라 테스트 메서드를 실행해서 코드가 맞는지 확인하는 것입니다.
given / when / then이란?
테스트 코드를 보면 주석이 세 부분으로 나뉘어 있습니다.
// given
// when
// then
이 구조는 테스트 코드를 읽기 쉽게 나누는 방식입니다.
| 구분 | 의미 |
|---|---|
| given | 테스트 준비 |
| when | 실제 실행 |
| then | 결과 확인 |
쉽게 말하면 이런 흐름입니다.
준비하고
실행하고
확인한다
given: 테스트 준비
MemberRepository memberRepository = new MemberRepository();
MemberService memberService = new MemberService(memberRepository);
여기서는 테스트에 필요한 객체를 준비합니다.
회원 등록 기능을 테스트하려면 MemberService가 필요합니다.
그리고 MemberService는 MemberRepository를 사용하므로 Repository도 함께 준비합니다.
즉, given은 테스트를 하기 위한 준비 단계입니다.
when: 실제 실행
memberService.join("kim");
List<Member> members = memberService.findMembers();
여기서는 실제로 테스트하고 싶은 기능을 실행합니다.
kim이라는 이름으로 회원을 등록하고,
저장된 회원 목록을 다시 조회합니다.
즉, when은 실제 행동을 하는 단계입니다.
then: 결과 확인
assertEquals(1, members.size());
assertEquals("kim", members.get(0).getName());
여기서는 결과가 맞는지 확인합니다.
첫 번째 줄은 회원 목록의 크기가 1인지 확인합니다.
assertEquals(1, members.size());
두 번째 줄은 첫 번째 회원의 이름이 kim인지 확인합니다.
assertEquals("kim", members.get(0).getName());
결과가 예상과 같으면 테스트는 성공합니다.
결과가 다르면 테스트는 실패합니다.
실패하는 테스트를 보면 더 이해하기 쉽다
예를 들어 아래처럼 일부러 잘못 적어보겠습니다.
assertEquals("park", members.get(0).getName());
실제로 저장한 이름은 kim인데,
테스트에서는 park을 기대하고 있습니다.
그러면 테스트는 실패합니다.
이 실패 메시지를 보고 우리는 알 수 있습니다.
내가 기대한 값과 실제 값이 다르구나.
테스트는 이렇게 코드가 맞는지 틀리는지 알려줍니다.
두 번째 테스트: 이름이 비어 있으면 에러
이제 이름이 비어 있을 때 에러가 나는지도 확인해보겠습니다.
package com.example.demo;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class MemberServiceTest {
@Test
void 회원가입을_하면_회원목록에_저장된다() {
// given
MemberRepository memberRepository = new MemberRepository();
MemberService memberService = new MemberService(memberRepository);
// when
memberService.join("kim");
List<Member> members = memberService.findMembers();
// then
assertEquals(1, members.size());
assertEquals("kim", members.get(0).getName());
}
@Test
void 이름이_비어있으면_회원가입을_할_수_없다() {
// given
MemberRepository memberRepository = new MemberRepository();
MemberService memberService = new MemberService(memberRepository);
// when & then
assertThrows(IllegalArgumentException.class, () -> {
memberService.join("");
});
}
}
assertThrows는 무엇일까?
assertThrows(IllegalArgumentException.class, () -> {
memberService.join("");
});
assertThrows는 특정 에러가 발생하는지 확인할 때 사용합니다.
여기서는 이름이 빈 문자열일 때
IllegalArgumentException이 발생해야 한다고 기대합니다.
Service 코드에는 이런 로직이 있었습니다.
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
}
그래서 빈 이름으로 회원가입을 시도하면 에러가 나야 정상입니다.
이것도 테스트 코드로 확인할 수 있습니다.
테스트 실행하기
IDE에서는 테스트 메서드 옆의 실행 버튼을 눌러 실행할 수 있습니다.
터미널에서도 실행할 수 있습니다.
Gradle 프로젝트라면:
./gradlew test
Maven 프로젝트라면:
./mvnw test
테스트가 성공하면 보통 초록색 표시가 나오거나, 빌드가 성공했다는 메시지가 나옵니다.
테스트가 실패하면 어떤 테스트가 실패했는지 알려줍니다.
브라우저로 확인하는 것과 테스트 코드의 차이
브라우저로 확인하는 방식은 사람이 직접 합니다.
예를 들어:
주소 입력
결과 확인
다른 주소 입력
다시 결과 확인
테스트 코드는 이 과정을 코드로 자동화합니다.
| 방식 | 특징 |
|---|---|
| 브라우저 확인 | 직접 눈으로 확인 |
| 테스트 코드 | 코드가 자동으로 확인 |
처음에는 브라우저 확인이 더 쉬워 보일 수 있습니다.
하지만 기능이 많아질수록 테스트 코드가 훨씬 편해집니다.
테스트 코드는 문서 역할도 한다
테스트 코드는 단순히 확인용만은 아닙니다.
예를 들어 아래 테스트 이름을 보면 기능이 무엇인지 알 수 있습니다.
void 회원가입을_하면_회원목록에_저장된다()
이 이름만 봐도 이 기능의 기대 동작을 알 수 있습니다.
또 다른 테스트도 마찬가지입니다.
void 이름이_비어있으면_회원가입을_할_수_없다()
이 테스트 이름은 회원가입 규칙을 설명해줍니다.
즉, 테스트 코드는 기능 설명서처럼 읽힐 수도 있습니다.
초보자가 헷갈리기 쉬운 부분
1. 테스트 코드는 실제 서버를 꼭 실행해야 할까?
이번 예제에서는 서버를 실행하지 않아도 됩니다.
MemberService와 MemberRepository 객체를 직접 만들어서 테스트하기 때문입니다.
즉, 브라우저로 접속하지 않고도 Service 로직을 확인할 수 있습니다.
2. 테스트 코드는 어디에 작성할까?
테스트 코드는 보통 src/test/java 아래에 작성합니다.
실제 코드는 src/main/java에 두고,
테스트 코드는 src/test/java에 두는 식으로 나눕니다.
3. 테스트 이름은 꼭 영어로 써야 할까?
꼭 그렇지는 않습니다.
Java 메서드 이름에 한글을 사용할 수도 있습니다.
초보 단계에서는 테스트 의도가 잘 보이도록 한글로 작성해도 괜찮습니다.
다만 팀이나 회사에서는 영어 네이밍 규칙을 정해두는 경우도 있습니다.
4. 테스트가 실패하면 나쁜 걸까?
나쁜 것이 아닙니다.
테스트 실패는 코드가 기대와 다르게 동작한다는 신호입니다.
오히려 빨리 실패를 확인할 수 있다는 것이 테스트 코드의 장점입니다.
오늘 배운 것
이번 글에서는 테스트 코드를 처음 작성해봤습니다.
핵심은 아래와 같습니다.
- JUnit은 Java 테스트 코드를 작성할 때 사용하는 도구
@Test가 붙은 메서드는 테스트로 실행됨- 테스트 코드는 보통
src/test/java에 작성함 - given / when / then은 준비, 실행, 확인 흐름을 나누는 방식
assertEquals는 값이 같은지 확인함assertThrows는 특정 예외가 발생하는지 확인함- 테스트 코드를 쓰면 브라우저로 직접 확인하지 않아도 기능을 검증할 수 있음
정리
테스트 코드는 처음에는 조금 낯설 수 있습니다.
하지만 핵심은 단순합니다.
내가 기대한 결과와 실제 결과가 같은지 코드로 확인한다.
브라우저로 직접 실행해보는 것도 중요하지만, 테스트 코드가 있으면 기능을 더 빠르고 안전하게 확인할 수 있습니다.
이번 글에서는 회원 등록 기능을 기준으로 저장 성공 테스트와 예외 테스트를 작성해봤습니다.
처음에는 모든 테스트를 완벽하게 작성하려고 하기보다, 작은 기능 하나를 코드로 확인하는 것부터 시작하면 됩니다.
다음 글에서는 Repository와 DB가 왜 필요한지, 메모리 저장소의 한계를 기준으로 정리해보겠습니다.