13장. 테스트 코드 작성하기
13.1 테스트란
테스트: 프로그램의 품질을 검증하는 것, 의도대로 프로그램이 잘 동작하는지 확인하는 과정
테스트 코드 작성
- 예상 데이터 작성하기
- 실제 데이터 획득하기
- 예상 데이터와 실제 데이터 비교해 검증하기
작성한 코드가 테스트를 통과하면 지속적인 리팩터링, 실패하면 디버깅
테스트 코드는 다양한 경우를 대비해 작성 (테스트 케이스)
테스트 케이스는 성공할 경우 뿐 아니라 실패할 경우도 고려하여 세부적으로 작성
테스트 주도 개발(TDD, Test Driven Development):
일단 테스트 코드를 만든 후 이를 통과하는 최소한의 코드부터 시작해 점진적으로 코드를 개선 및 확장해 나가는 개발 방식
13.2 테스트 코드 작성하기
테스트 코드 기본 틀 만들기
service > ArticleService 열기
테스트하고 싶은 메서드에서 우클릭 후 Generate → Test 선택
Create Test 창에서 JUnit5 선택 후, Member에서 체크 후 [OK] 버튼 클릭
example.firstproject > service > ArticleServiceTest 로 이름과 경로가 자바 코드와 동일하게 생성됨
package com.example.firstproject.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ArticleServiceTest {
@Test
void index() {
}
}
테스트 코드를 스프링 부트와 연동하기 위해 설정
package com.example.firstproject.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleServiceTest {
@Autowired
ArticleService articleService;
@Test
void index() {
}
}
ArticleServiceTest 클래스 위에 @SpringBootTest 붙임
ArticleService 클래스를 테스트하기 위해 articleService 객체 선언, @Autowired 붙임
index() 테스트하기
package com.example.firstproject.service;
import com.example.firstproject.entity.Article;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleServiceTest {
@Autowired
ArticleService articleService;
@Test
void index() {
// 1. 예상 데이터
Article a = new Article(1L, "가가가가", "1111");
Article b = new Article(2L, "나나나나", "2222");
Article c = new Article(3L, "다다다다", "3333");
List<Article> expected = new ArrayList<Article>(Arrays.asList(a, b, c));
// 2. 실제 데이터
List<Article> articles = articleService.index();
// 3. 비교 및 검증
assertEquals(expected.toString(), articles.toString());
}
}
예상 데이터와 실제 데이터가 일치하는지 비교하여 검증
Article c = new Article(4L, "다다다다", "3333"); 으로 3L → 4L로 수정하면 테스트에 통과하지 못했다고 나옴
show() 테스트하기
존재하는 id를 입력해 게시글 조회에 성공하는 경우 테스트
@Test
void show_성공_존재하는_id_입력() {
// 1. 예상 데이터
Long id = 1L;
Article expected = new Article(id, "가가가가", "1111");
// 2. 실제 데이터
Article article = articleService.show(id);
// 3. 비교 및 검증
assertEquals(expected.toString(), article.toString());
}
예상 데이터는 사용자가 ID가 1인 게시물의 조회를 요청했다고 가정하여 작성
실행하면 테스트에 잘 통과
예상 데이터를 일부러 틀려 title을 다르게 수정할 시 테스트에 통과하지 못함
존재하지 않는 id를 입력해 실패하는 경우 테스트
@Test
void show_실패_존재하지_않는_id_입력() {
// 1. 예상 데이터
Long id = -1L;
Article expected = null;
// 2. 실제 데이터
Article article = articleService.show(id);
// 3. 비교 및 검증
assertEquals(expected, article);
}
예상 데이터를 존재하지 않는 id인 -1을 조회한다고 가정하여 작성
DB에 조회되는 내용이 없어 null을 반환할 것이므로 expected 객체에 null 저장
테스트 돌려보면 실제로 null을 반환하기때문에 테스트에 잘 통과
create() 테스트하기
title과 content만 있는 dto를 입력한 경우 테스트 (성공 상황)
@Test
void create_성공_title과_content만_있는_dto_입력() {
// 1. 예상 데이터
String title = "라라라라";
String content = "4444";
ArticleForm dto = new ArticleForm(null, title, content);
Article expected = new Article(4L, title, content);
// 2. 실제 데이터
Article article = articleService.create(dto);
// 3. 비교 및 검증
assertEquals(expected.toString(), article.toString());
}
사용자가 새 게시물을 생성한 상황을 가정하여 예상 데이터 작성
id는 DB가 자동으로 생성하므로 title과 content 필드만 선언하고 dto 객체로 생성
예상 데이터의 id는 자동으로 생성될 값인 4L를 써 줌
실제 데이터와 비교, 테스트를 돌려 보면 정상적으로 통과
id가 포함된 dto가 입력된 경우 테스트 (실패 상황)
@Test
void create_실패_id가_포함된_dto_입력() {
// 1. 예상 데이터
Long id = 4L;
String title = "라라라라";
String content = "4444";
ArticleForm dto = new ArticleForm(id, title, content);
Article expected = null;
// 2. 실제 데이터
Article article = articleService.create(dto);
// 3. 비교 및 검증
assertEquals(expected, article);
}
예상 데이터는 사용자가 새 게시물을 생성할 때 id를 써 줄 필요가 없는데도 id를 입력한 상황
게시글 생성할 때 id를 넣으면 오류가 나고 null이 반환되므로 expected 객체에 null 저장
비교할 때 null은 toString() 메서드를 호출할 수 없으므로 expected, article로 씀
테스트를 돌려 보면 정상적으로 통과
여러 테스트 케이스 한 번에 실행하기
각 테스트 케이스를 돌리지 않고 ArticleServiceTest 클래스 자체를 실행하면 여러 테스트 케이스를 한 번에 돌릴 수 있음
Run 'ArticleServiceTest' 선택
→ index() 통과하지 못함
예상 데이터 값에 3개의 데이터가 있는데, 실제 데이터 값이 4개가 있음
→ 롤백하지 않아 생긴 문제
index() 메서드 실행 전에 다른 테스트 케이스 메서드를 실행하면서 데이터를 하나 추가하였기때문에 테스트가 끝나면 롤 백하도록 해야 함
데이터를 조회하는 테스트를 제외하고 데이터를 생성, 수정, 삭제하는 테스트를 할 때는 롤백 처리를 해주어야 함
메서드 위에 @Transactional 을 추가하여 테스트가 끝난 후 변경된 데이터를 처음으로 되돌리도록 함
package com.example.firstproject.service;
import com.example.firstproject.dto.ArticleForm;
import com.example.firstproject.entity.Article;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleServiceTest {
@Autowired
ArticleService articleService;
@Test
void index() {
// 1. 예상 데이터
Article a = new Article(1L, "가가가가", "1111");
Article b = new Article(2L, "나나나나", "2222");
Article c = new Article(3L, "다다다다", "3333");
List<Article> expected = new ArrayList<Article>(Arrays.asList(a, b, c));
// 2. 실제 데이터
List<Article> articles = articleService.index();
// 3. 비교 및 검증
assertEquals(expected.toString(), articles.toString());
}
@Test
void show_성공_존재하는_id_입력() {
// 1. 예상 데이터
Long id = 1L;
Article expected = new Article(id, "가가가가", "1111");
// 2. 실제 데이터
Article article = articleService.show(id);
// 3. 비교 및 검증
assertEquals(expected.toString(), article.toString());
}
@Test
void show_실패_존재하지_않는_id_입력() {
// 1. 예상 데이터
Long id = -1L;
Article expected = null;
// 2. 실제 데이터
Article article = articleService.show(id);
// 3. 비교 및 검증
assertEquals(expected, article);
}
@Transactional
@Test
void create_성공_title과_content만_있는_dto_입력() {
// 1. 예상 데이터
String title = "라라라라";
String content = "4444";
ArticleForm dto = new ArticleForm(null, title, content);
Article expected = new Article(4L, title, content);
// 2. 실제 데이터
Article article = articleService.create(dto);
// 3. 비교 및 검증
assertEquals(expected.toString(), article.toString());
}
@Transactional
@Test
void create_실패_id가_포함된_dto_입력() {
// 1. 예상 데이터
Long id = 4L;
String title = "라라라라";
String content = "4444";
ArticleForm dto = new ArticleForm(id, title, content);
Article expected = null;
// 2. 실제 데이터
Article article = articleService.create(dto);
// 3. 비교 및 검증
assertEquals(expected, article);
}
}
테스트 코드 실행 오류
IntelliJ Settings에서 Gradle 검색 후 Run tests using: Gradle (Default) → IntelliJ IDEA로 바꾸니 테스트 코드 정상적으로 실행됨
왜 그런가에 대한 ChatGPT 의 답변 >
IntelliJ에서 Run tests using 설정을 Gradle에서 IntelliJ로 변경했을 때 테스트 실행이 정상적으로 된 이유는 다음과 같은 환경 차이 때문입니다.
1. Gradle과 IntelliJ의 테스트 실행 환경 차이
Gradle
- Gradle을 사용해 테스트를 실행하면 Gradle이 별도의 JVM과 빌드 환경을 설정하여 테스트를 실행합니다.
- Gradle의 의존성 관리와 빌드 설정이 정확하지 않으면 테스트가 실패하거나 실행되지 않을 수 있습니다.
- 예: Gradle 버전이 오래되었거나, build.gradle 파일에 테스트와 관련된 설정이 누락된 경우.
- Gradle이 관리하는 JVM과 IntelliJ가 사용하는 JVM 버전이 다를 수도 있습니다.
IntelliJ
- IntelliJ로 테스트를 실행하면 IDE가 제공하는 기본 실행 환경과 JVM을 사용합니다.
- IntelliJ는 프로젝트의 classpath를 직접 참조하므로, Gradle 설정의 영향을 받지 않고 실행됩니다.
- 빠른 피드백 루프를 제공하기 위해 IntelliJ는 내부적으로 간소화된 방식으로 테스트를 실행합니다.
2. 문제의 원인
Gradle로 테스트 실행 시 문제가 발생했던 이유는 여러 가지가 있을 수 있습니다:
(1) Gradle 테스트 설정 문제
- build.gradle 파일에 테스트와 관련된 설정이 누락되었거나, Gradle이 필요한 플러그인/의존성을 제대로 로드하지 못한 경우.
- 예: testImplementation으로 테스트 라이브러리를 추가하지 않았거나, tasks.test 설정이 잘못된 경우.
(2) Gradle의 JVM 버전 문제
- Gradle이 사용하는 JVM 버전이 프로젝트에서 요구하는 버전과 맞지 않을 경우, 테스트 실행이 실패할 수 있습니다.
- 예: 프로젝트가 Java 17을 요구하지만 Gradle이 Java 11로 실행 중인 경우.
(3) 캐시 문제
- Gradle의 빌드 캐시가 손상되었거나 오래된 경우, 의존성이 제대로 로드되지 않아 테스트가 실패할 수 있습니다.
- ./gradlew clean 명령어로 캐시를 초기화하면 해결될 수 있음.
(4) IntelliJ와 Gradle 간 classpath 불일치
- IntelliJ와 Gradle 간에 classpath 설정이 다르게 되어 있을 경우, Gradle로 실행하면 필요한 의존성을 찾지 못하는 반면, IntelliJ에서는 잘 동작할 수 있습니다.
3. 왜 IntelliJ로 실행하면 잘 동작했는가?
- IntelliJ는 프로젝트의 설정(.iml 파일 및 IDE 설정)을 기반으로 테스트를 실행하므로, Gradle 설정에 문제가 있더라도 영향을 덜 받습니다.
- IntelliJ의 내부 테스트 실행 환경은 더 직관적이고, 특별한 추가 설정 없이도 대부분의 테스트를 실행할 수 있습니다.
4. 해결 방법
Gradle에서도 테스트를 올바르게 실행할 수 있도록 다음 단계를 확인하세요:
(1) Gradle 테스트 설정 확인
- build.gradle 파일에 필요한 테스트 의존성이 모두 추가되었는지 확인하세요.
dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' }
(2) Gradle JVM 확인
- Gradle이 사용하는 JVM 버전을 확인하고, 프로젝트에 맞는 버전인지 점검하세요.
필요한 경우 IntelliJ에서 Gradle JVM 설정을 변경하세요:./gradlew -version
- Settings > Build, Execution, Deployment > Build Tools > Gradle > Gradle JVM.
(3) Gradle 캐시 정리
- Gradle의 빌드 캐시를 정리하여 문제가 되는 잔여 데이터를 제거하세요.
./gradlew clean
(4) Gradle과 IntelliJ 동기화
- IntelliJ에서 Gradle 프로젝트를 동기화하여 모든 설정이 최신 상태로 유지되도록 합니다:
- File > Sync Project with Gradle Files 클릭.
(5) Gradle 테스트 실행
- Gradle에서 직접 테스트를 실행해 문제가 해결되었는지 확인합니다.
./gradlew test
요약
- IntelliJ로 테스트 실행 시 IDE가 Gradle 설정을 우회하므로 더 간단하게 테스트를 실행할 수 있습니다.
- Gradle 테스트가 실패했다면 설정 문제, JVM 버전 불일치, Gradle 캐시 등이 원인일 가능성이 높습니다.
- Gradle 설정을 점검하고, IntelliJ와 Gradle 간 환경을 일치시키면 Gradle에서도 문제가 해결될 것입니다. 😊