[Spring] TDD : JUnit으로 단위 테스트와 통합 테스트 정리

이지연·2026년 2월 10일

TDD(Test Driven Development)란?

TDD는 테스트의 중요성을 강조한 개발 방법론으로, 테스트 코드를 먼저 설계하고 작성한 뒤 그 테스트를 통과시키는 방향으로 실제 기능을 개발해 나간다. 즉 “구현 먼저”가 아니라 “검증 기준(테스트) 먼저”를 세우는 방식이다.

테스트를 먼저 작성하면 개발자는 원하는 기능과 동작 방식을 더 명확하게 정의하게 된다. 또한 결함을 초기에 발견할 가능성이 커져, 장기적으로 유지보수 비용과 리스크를 줄이고 개발 효율성을 높이는 데 도움이 된다.


스프링에서 JUnit을 많이 쓰는 이유

스프링 기반 프로젝트에서 테스트 프레임워크로 JUnit이 가장 흔하게 사용된다. 단위 테스트부터 통합 테스트까지 폭넓게 적용할 수 있고, 스프링 테스트 지원(@SpringBootTest 등)과 함께 사용할 때 워크플로우가 자연스럽기 때문이다.

이 글에서는 테스트를 “단위 테스트”와 “통합 테스트”로 나누어 정리하고, 마지막에 실습 코드를 통해 Repository/Service 테스트가 어떻게 작성되는지까지 같이 본다.


테스트 방법 1) 단위 테스트(Unit Test)

단위 테스트는 애플리케이션의 가장 작은 단위(메서드, 클래스)를 검증한다. 핵심은 테스트 대상 외의 의존성을 줄이고, 빠르고 자주 실행 가능한 형태로 만드는 것이다.

Controller 테스트

컨트롤러 테스트는 외부 의존성을 직접 붙이지 않고 목킹(Mocking) 으로 가상화하는 방식이 많이 쓰인다.

  • Mock(모의 객체)이란 실제 객체를 흉내 내는 가짜 객체로, 단위 테스트에서 실제 구현을 대체하기 위해 사용한다
  • 스프링 MVC 계층 테스트에서는 MockMvc 객체를 주로 활용해 요청/응답 흐름을 검증한다

컨트롤러는 “요청을 어떤 서비스로 전달하고, 어떤 응답을 만드는지”가 핵심이므로, 서비스 계층을 mock 처리하고 컨트롤러의 책임만 검증하는 방식이 효율적이다.

Service 테스트

서비스 계층 테스트에서는 실제 서비스 로직을 검증한다. 이때 트랜잭션을 걸어 테스트가 끝난 뒤 롤백되도록 구성하는 경우가 많다.

  • 실제 서비스 계층과 마찬가지로 @Transactional을 사용
  • 테스트 종료 후 트랜잭션 롤백으로 데이터 정합성을 깔끔하게 유지 (스프링 테스트는 기본적으로 트랜잭션 테스트의 롤백 동작을 지원하며, @Rollback / @Commit으로 제어 가능)

즉, 서비스 테스트에서는 “로직이 맞게 동작하는지”에 집중하면서도, DB 변경이 테스트 간 간섭을 만들지 않도록 관리하는 것이 포인트다.

Repository 테스트

리포지토리 테스트는 JPA 동작을 검증하는 경우가 많아, JPA 관련 설정만 가볍게 로드하는 어노테이션을 사용한다.

  • @DataJpaTest를 많이 사용
  • @DataJpaTest는 엔티티/리포지토리 스캔 중심으로 슬라이스 테스트를 구성하고, 기본적으로 트랜잭션 롤백 동작을 제공한다

리포지토리는 “DB와의 상호작용이 기대대로 되는지”를 확인하는 계층이므로, 애플리케이션 전체를 띄우지 않고도 필요한 설정만 로딩해 테스트 속도와 안정성을 확보한다.


테스트 방법 2) 통합 테스트(Integration Test)

통합 테스트는 애플리케이션의 여러 컴포넌트가 함께 동작할 때 전체 흐름이 올바르게 작동하는지 확인한다.

  • @SpringBootTest 어노테이션을 사용해 스프링 컨텍스트를 로드
  • 각 테스트 메서드 단위로 실행 가능하고, 클래스 단위로도 실행 가능

여기서 주의할 점은 클래스 단위 실행 시 메서드 간 의존성이 생기지 않게 해야 한다는 것이다. 예를 들어 특정 테스트가 데이터를 추가하고, 다음 테스트가 그 데이터에 기대면 테스트 순서에 따라 결과가 달라질 수 있다.

이를 방지하기 위해 다음과 같은 정리 작업을 한다.

  • @AfterEach를 사용해 테스트에서 추가된 데이터 삭제 등 “테스트 간 의존성 제거” 작업 수행

자주 쓰는 테스트 작성 패턴: Given-When-Then

테스트는 가독성이 중요하고, 의도를 명확히 드러내야 한다. 그럴 때 Given-When-Then 패턴(준비-실행-검증)을 많이 사용한다.

  • Given: 테스트에 필요한 조건과 데이터를 준비
  • When: 실제로 테스트할 동작을 실행
  • Then: 결과가 기대대로인지 검증(assert)

예를 들어 은행 계좌에서 출금을 테스트한다면 다음처럼 정리할 수 있다.

  • Given(준비): 계좌에 1000달러가 있다
  • When(실행): 500달러 출금을 실행한다
  • Then(검증): 잔액이 500달러인지 확인한다

예시 코드(개념 예시):

// Given
account = new BankAccount(1000);

// When
account.withdraw(500);

// Then
assert account.balance == 500;

이 구조를 유지하면 테스트가 “무엇을 준비했고, 무엇을 실행했고, 무엇을 기대하는지” 한눈에 들어오며, 실패했을 때 원인도 빨리 추적할 수 있다.


실습 코드: Repository/Service 테스트 붙이기

아래 코드는 지금까지 정리한 개념(통합 테스트 환경에서의 스프링 컨텍스트 로딩, 트랜잭션 롤백, Given-When-Then)을 그대로 적용한 예시다. 특히 @SpringBootTest + @Transactional 조합은 테스트가 끝나면 트랜잭션이 롤백되도록 구성하는 데 자주 사용되며, 롤백 여부는 @Rollback(또는 @Commit)으로 제어할 수 있다.

1) Repository 테스트 예시

  • 포인트: “저장 → 조회 → 동일성(또는 동일 값) 검증” 흐름으로 리포지토리 동작을 검증한다
  • 참고: 레포지토리 테스트는 상황에 따라 @SpringBootTest 대신 @DataJpaTest를 써서 더 가볍게 가져갈 수도 있다
package com.beyond.basic.author;

import com.beyond.basic.author.domain.Author;
import com.beyond.basic.author.repository.AuthorRepository;
import org.junit.jupiter.api.Assertions;
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;

@SpringBootTest
@Transactional  // Transactional 어노테이션 + Test = DB자동 롤백처리
//@DataJpaTest  // jpa관련된 의존성들만 사용. 주로 repository테스트에사용
public class AuthorRepositoryTest {
    private final AuthorRepository authorRepository;

    @Autowired
    public AuthorRepositoryTest(AuthorRepository authorRepository) {
        this.authorRepository = authorRepository;
    }

    @Test
    public void authorSaveTest() {
        // 테스트 검증 로직: 객체 생성(저장 전) -> save -> 저장된 객체(DB 조회)를 조회하여 저장전의 객체와 비교
        // - given when then 패턴(prepare execute then 패턴)

        // (1) 준비(prepare, given)
        Author author = Author.builder()
                .name("abc")
                .email("abc@naver.com")
                .password("12341234")
                .build();

        // (2) 실행(execute, when)
        authorRepository.save(author);

        // (3)검증(then)
        Author authorDb = authorRepository.findByEmail("abc@naver.com").orElse(null);
        Assertions.assertEquals(author.getEmail(), authorDb.getEmail());
    }
}

여기서 Assertions.assertEquals()는 기대값(expected)과 실제값(actual)이 같은지 검증할 때 쓰는 대표적인 JUnit Assertion이다.

2) Service 테스트 예시

  • 포인트: 서비스 메서드를 통해 저장 로직이 정상 동작하는지 검증하고, 조회 로직(findAll 등)이 기대한 개수를 반환하는지 검증한다
  • 주의: findAll 개수 테스트처럼 “기존 데이터 개수에 의존”하는 테스트는 실행 환경에 따라 초기 데이터가 달라질 수 있으니, 테스트 데이터 셋업/정리 전략을 같이 설계하는 편이 안전하다
package com.beyond.basic.author;

import com.beyond.basic.author.domain.Author;
import com.beyond.basic.author.dto.AuthorCreateDto;
import com.beyond.basic.author.repository.AuthorRepository;
import com.beyond.basic.author.service.AuthorService;
import org.junit.jupiter.api.Assertions;
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;

@SpringBootTest
@Transactional
public class AuthorServiceTest {
    private final AuthorService authorService;
    private final AuthorRepository authorRepository;

    @Autowired
    public AuthorServiceTest(AuthorService authorService, AuthorRepository authorRepository) {
        this.authorService = authorService;
        this.authorRepository = authorRepository;
    }

    @Test
    public void authorSaveTest() {
//        given
        AuthorCreateDto dto = AuthorCreateDto.builder()
                .name("abc")
                .email("abc@naver.com")
                .password("12341234")
                .build();
//        when
        authorService.save(dto);
//        then
        Author authorDb=authorRepository.findByEmail("abc@naver.com").orElse(null);
        Assertions.assertEquals(authorDb.getEmail(), dto.getEmail());
    }

    @Test
    public void authorFindAllTest() {
//        findAll을 통한 결과값의 개수가 맞는지를 검증
//        최초개수 구하고
        int beforeSize = authorService.findAll().size();
//        데이터 추가(3개)
        authorRepository.save(Author.builder().name("h1").email("h1@naver.com").password("12341234").build());
        authorRepository.save(Author.builder().name("h2").email("h2@naver.com").password("12341234").build());
        authorRepository.save(Author.builder().name("h3").email("h3@naver.com").password("12341234").build());
//        db에서 findAll을 통해 총 개수를 구하여 최초개수 +3하고 일치하는지 검증
        int afterSize = authorService.findAll().size();
        Assertions.assertEquals(beforeSize+3, afterSize);
    }
}

@Transactional을 테스트에 적용했을 때 “테스트 메서드 수행 후 롤백”이 기본 동작인지, 또는 특정 테스트만 커밋되게 할지(@Rollback(false) 또는 @Commit) 같은 운영 방식은 프로젝트 규칙에 맞춰 정하면 된다.


IntelliJ에서 테스트 실행(클래스/메서드)

IntelliJ IDEA에서는 테스트 클래스 또는 테스트 메서드 왼쪽 여백(gutter)에 보이는 실행 아이콘(초록색 재생 버튼)을 통해 바로 실행할 수 있다. 테스트 클래스 이름 옆 아이콘을 누르면 해당 클래스의 테스트가 전체 실행된다.

특정 테스트만 돌리고 싶다면, 실행하려는 @Test 메서드 옆 아이콘을 눌러 메서드 단위 실행을 하면 된다. 또한 테스트 코드 화면에서 클래스/메서드에 커서를 둔 상태로 실행 메뉴를 사용하면 “현재 컨텍스트(선택 위치)” 기준으로 클래스/메서드 단위 실행이 가능하다.

추가로, 이전에 실행했던 테스트는 상단의 Run 버튼을 누르면 “마지막 실행 구성”으로 다시 실행되며, 필요하면 Run/Debug Configuration에서 JUnit 실행 대상을 클래스/패턴 등으로 지정해 관리할 수도 있다.

profile
Eazy하게

0개의 댓글