[Groupware] Spring-Boot : 테스트 코드

2025. 6. 26. 16:42·Project/Backend

📝 개요

이번 프로젝트는 Spirng Boot 기반의 REST API 서버를 구축하고 기본을 익히는 과정이다. 초반에는 각 도메인에 대한 CRUD 를 개발하고 기능을 추가하는데 집중했지만, 점점 기능이 많아지고 API 요청/ 응답 케이스가 다양해지면서, 직접 서버를 띄워 일일이 Postman 으로 요청을 날리는 방식으로는 한계를 느꼈다.

 

API 마다 다양한 예외 상황과 비즈니스 로직을 수동으로 검증하다보니 작업 속도도 느려지고, 코드 수정 시 기존 기능이 깨지는 경우도 있었다. 그래서 REST API 서버를 구축하면서 테스트 코드라는 것을 알게 되었고, 이를 적용하여 개발 속도를 높이고 각 예외에 대해서 더 견고하게 확인하고 설계하기 위해 도입했다.

 

다음은 프로젝트에 적용된 Spirng Boot 기반의 백엔드 서버에서의 REST API 에 대한 테스트 코드를 설명한다.


🚀 테스트 코드

1️⃣ 개념

테스트 코드는 소프트웨어가 의도한 대로 동작하는지 자동으로 검증하는 코드이다. 테스트를 직접 수동으로 하는게 아니라 코드를 통해 자동으로 실행하여 결과를 체크하기 때문에 실수를 줄이고 서비스 품질을 높일 수 있다. 테스트 코드를 작성함으로써 자동화와 효율성을 챙길 수 있고 요구사항에 대한 명확한 정리도 자연스럽게 이루어진다.

 

Spirng 기반 백엔드 프로젝트에서의 테스트 코드는 단순히 “함수가 제대로 동작하나?” 의 수준이 아니고 Spring 의 다양한 컴포넌트/레이어 (Controller, Serivce, Repository 등), 그리고 실제 웹 요청, DB 연동, 인증/권한, 트랜잭션 등 “실서비스 환경” 과 유사한 다양한 조건까지 자동으로 검증할 수 있는 엔터프라이즈용 품질 보증 도구라고 할 수 있다.

 

[ Spirng 백엔드에서의 테스트 코드 ]

  1. 계층별(레이어별) 독립 테스트 가능
    • 단순 자바 메서드 단위(unit test) 테스트
    • Controller(HTTP 요청 / 응답) 테스트
    • Service(비즈니스 로직) 테스트
    • Repository / Mapper (DB 연동) 테스트
    • 통합(Integration) 등 레이어별 동작과 상호작용 테스트
  2. 스프링 DI, 빈 주입, 시큐리티, 트랜잭션까지 포함
    • 단순 객체가 아니라 스프링의 IoC 컨테이너가 실제로 빈을 주입/관리하는 상황에서 각종 AOP(트랜잭션), 시큐리지(권한), 검증, 실제로 돌아가는 환경과 거의 동일하게 테스트 가능
  3. 자동화된 검증 & CI/CD 연동
    • 모든 테스트는 @Test 등 어노테이션 기반으로 실행되어 Build, 배포, PR, merge 단계에서 자동 실행됨
    • 코드 퀄리티와 배포 안정성 확보 가능

2️⃣ 종류

실제로는 테스트라는 큰 틀안에서 정말 많은 테스트들이 분류되고, 테스트 코드들이 정의 되는데 지금 이 부분에서는 Spring 기반 백엔드 기준으로 설명한다.

  1. 단위테스트 (Unit Test)
    • 가장 작은 코드 단위(메서드, 클래스 등) 외부 환경과 분리해서 검증
    • Service 에서 Mapper, 외부 API 호출 등은 모두 Mock으로 대체하고 로직 자체만 테스트
    • 비즈니스 로직이 정확히 동작하는지 확인, 버그가 발생해도 어디서 발생한지 빠른 추적 가능
    • 테스트가 빠르고, 코드 변경 시 원인 추적에 용이하며 외부 영향이 없음 (실제 DB, 네트워크 미사용)
    • 너무 세분화 하거나 Mock 으로 대체된 부분이 많을수록 진짜 서비스 환경과 차이 발생
    • TDD/리팩토링 등 빈번한 코드 수정 구간에서 적극 사용
  2. 통합 테스트 (Integration Test)
    • 여러 컴포넌트(Controller-Service-Repository) 또는 실제 DB/외부 시스템 등 전체 흐름 검증
    • @SpringBootTest 등으로 스플이 전체 환경을 띄우고 진짜 DB/트랜잭션/빈주입/시큐리티 등 실제 서비스와 유사하게 테스트
    • 각 레이어/의존성이 제대로 연동되는지, 실제 DB/외부 API 연동, 트랜잭션 롤백 등 서비스 전체 품질 체크
    • Mock 없이 실제 환경 테스트 가능, 실서비스와 매우 유사, 배포 전 장애/문제를 조기 발견 가능
    • 테스트 속도가 느리고 데이터 세팅, 환경 셋업이 복잡할 수 있음
    • 배포 전 전체 품질/연동 테스트, 비즈니스 흐름 (회원가입-로그인-조회 등) 자동 검증 시 사용
  3. 컨트롤러(웹 레이어) 테스트 (Web/Controller Test)
    • HTTP 요청/ 응답, 파라미터/유효성, 에러처리, 인증 등 컨트롤러 계층의 API 가 정상 동작하는지 테스트
    • @WebMvcTest + MockMvc 등으로 컨트롤러만 단독 실행, 나머지는 Mock 처리
    • API 스펙, 입력값 검증, 응답 형태, 인증/권한 체크, 에러 핸들링 등 API 품질 보증
    • 컨트롤러 로직만 집중, 빠른 실행 가능
    • MockMvc 로 실제 HTTP 요청/응답과 유사하게 검증 가능
    • 필드/파라미터 유효성, 예외처리, 시큐리티 핸들링까지 커버
    • DB Service 로직은 Mock 으로 대체함 (실제 연동은 하지 않음)
    • API 개발/변경 시 외부 계약(API 스펙) 보장, 입력값 유효성, 에러 포맷 등 UI/FE 연동 테스트 시 사용
  4. 엔드투엔드 (E2E, 인수 테스트, 시나리오 테스트)
    • 실제 유저 관점에서, 시스템 전체(프론트백 ~ DB ~ 외부 API) 시나리오대로 동작하는지 자동화 검증
    • Spirng 단독보다는 Cypress, Selenium, RestAssured 등 도구화 병행 가능
    • 실서비스 관점의 완전한 시나리오 품질 보장 / 진짜 배포 환경과 유사하고 복잡한 인수/사용자 시나리오 까지 검증
    • 테스트 속도가 느리고 환경/데이터/의존성이 복잡함, 운영 환경과 동기화 문제
    • 전체 시나리오 QA, 주요 기능 릴리즈 전 최종 품질 점검

🚀 프로젝트 적용

1️⃣ 테스트 코드 작성 기준/패턴

Groupware 프로젝트에서는 서비스 코드에 대한 단위 테스트(비즈니스 로직 검증) 와 컨트롤러 코드에 대한 컨트롤러 테스트(API 요청 및 응답, 유효성 검증)을 진행한다.

 

[ 네이밍 규칙 ]

  • 테스트 클래스명 : {테스트 대상 도메인 서비스/컨트롤러}Test 로 작성
  • 클래스명 : @DisplayName(”도메인 기능”) {도메인_기능}
  • 메서드명 : @DisplayNmae(”도메인 기능” : 성공/실패 - 케이스) 성공/실패_실패케이스

 

[ 테스트 결과 DisplayName 표시 화면 ]

 

[ 정리 ]

  • 기능별 그룹핑 (@Nested) 사용, 성공/실패 케이스 분리, 독립적인 예외 조건으로 테스트
  • Mock/BDD 스타일 테스트 코드 패턴
  • Service 테스트 : 외부 의존성 (Mock) 으로 대체, DB, 외부 API는 전부 Mocking
  • Controller 테스트 : MockMvc로 HTPP 요청/응답을 실제처럼 검증 / 서비스는 MockitoBean 으로 대체

BDD(Behavior-Driven Development) 란?
BDD는 행위 주도 개발이라고 번역하며 시스템(코드)이 어떻게 동작(행위/시나리오) 해야 하는지 자연어로 명확하게 표현하고, 그에 맞춰 테스트 코드를 먼저 작성해서 개발/테스트/협업의 기준점을 만드는 개발 방법론이다.

”이 기능이 이런 상황에서 이런 결과가 나와야 한다” 를 사람(기획/개발/테스트) 모두가 이해할 수 있게 명확하게 적고, 이를 그대로 테스트 코드(특히 Given-When-Then 구조)로 녹여낸다.

[ Spring/JUnit 에서의 BDD ]
Given : “어떤 상황/데이터가 주어졌을 때”
When : “어떤 행위(함수/API 호출)를 했을 때”
Then : “이런 결과/상태가 나와야 한다”

기존의 when(…).thenReturn(…) 대신 BDD 스타일의 given(…).willReturn(…), then(…).should() 로 작성한다.


2️⃣ Service, Controller 계층별 테스트

[ Service Test ]

  1. BDDMokito 패턴 (given / then / willReturn / should /never)
    • given(…).willReturn(…) : Mock 객체의 동작 정의
      • given() 의 인자로는 동작에 대한 메서드가 들어감
      • willReturn() 의 인자로는 동작에 대한 예상 결과 값이 들어감
      • 내부적으로 Proxy 객체의 “호출 기록/리턴값/예외 등 스텁 동작” 을 지정
      • 예시 : given(userMapper.isUserExist(userId)).willReturn(false)
    • when 단계
      • Mock 객체의 호출 (검증하고자 하는 메서드 혹은 서비스 동작을 하는 메서드)
      • 내부에서 의존성으로 주입된 Mock 객체의 메서드를 실제로 호출하게 됨
      • 미리 given 에서 정의해둔 리턴값/예외가 실행됨
    • then(…).should()
      • Mock 객체가 실제로 호출되었는지 검증
      • should() 에 never() 를 인자로 주어 호출되지 않음을 검증
      • 예시 : then(userMapper).should().insertUser(any(UserVO.class));
  2. any(), eq(), ArgumentMatchers 계열
    • any, any(클래스명.class)
      • 어떤 값이든 허용, 타입만 맞으면 됨
      • 파라미터 값이 매번 달라지거나, 테스트 대상과 직접적인 상관이 없을 때 사용
      • 예시 : any(), any(Long.class), any(UserVO.class)
    • eq(value)
      • 정확하게 이 값이 들어왔는지(동등성) 검증, 파라미터가 특정 값으로 호출되었는지 체크 시 사용
      • 예시 : eq(USER_ID)
    • Mock 객체 메서드 호출 시 모든 인자(파라미터는 전부 ArgumentMathcer 로 통일해야함
  3. assertThat, assertThatThrownBy (AssertJ 라이브러리)
    • assertThat(value).isEqualTo(expectValue)
      • 결과 값이 기대값과 같은지 검증
    • assertThatThrwonBy(() → …).isinstanceOf(…).hasMessage(…)
      • 특정 코드 실행 시, 예외가 발생하는지(타입/메세지까지) 검증
# ArgumentMatcher 로 통일되지 않은 경우 에러 발생 
This exception may occur if matchers are combined with raw values: 
	//incorrect: someMethod(any(), "raw String"); 
When using matchers, all arguments have to be provided by matchers. For example: 
	//correct: someMethod(any(), eq("String by matcher"));

 

[ 실제 적용 코드 ]

@Nested
@DisplayName("사용자 등록")
class 사용자_등록 {

    // ... BeforeEach

    @Test
    @DisplayName("사용자 등록 : 성공")
    void 성공() {
        // given
        setAuthentication(USER_ID, "ROLE_ADMIN");
        given(userMapper.isUserExist(defaultRegisterReqDTO.getUserId())).willReturn(0);

        // ... 

        // when
        userService.registerUser(defaultRegisterReqDTO);

        // then
        then(userMapper).should().insertUser(any(UserVO.class));
    }

    @Test
    @DisplayName("사용자 등록 : 실패 - 권한 없음")
    void 실패_권한_없음() {
        // given
        setAuthentication(USER_ID, "ROLE_USER");  // 일반 사용자 권한

        // when & then
        assertThatThrownBy(() -> userService.registerUser(defaultRegisterReqDTO))
                .isInstanceOf(CustomException.class)
                .hasMessage(ErrorCode.FORBIDDEN.getMessage());  // CustomException ->  FORBIDDEN

        then(userMapper).should(never()).insertUser(any());  // 사용자 등록 호출 검증
    }
}

3️⃣ 어노테이션 및 적용 라이브러리

[ Class 단위 설정 어노테이션 ]

  1. @ExtendWith(MockitoExtension.class)
    • JUnit5 에서 Mockito 를 사용할 때, Mockito 의 확장(Extension) 기능을 활성화 하는 어노테이션
    • 테스트 클래스에 이 어노테이션이 있으면 내부의 @Mock, @InjectMocks 등 Mockito 관련 어노테이션이 자동으로 동작함
    • Mock 객체를 자동으로 생성/주입해서 테스트 환경을 쉽게 만들어줌
  2. @WebMvcTest
    • Spring MVC(Controller) 계층만 슬라이스해서 테스트할 때 사용하는 어노테이션
    • 실제로 스프링 전체 애플리케이션 컨텍스트를 띄우지 않고, 컨트롤러/관련된 MVC 컴포넌트(Controller, @ControllerAdvice, Filter, Interceptor 등) 만 최소한으로 로드해서 테스트 환경을 가볍게 만듦
    • Service, Repository 등은 MockBean 등으로 주입해서 비즈니스 로직이나 DB는 실제로 동작하지 않고, HTTP 요청/응답, 파라미터, 유효성, 예외처리 등 API 스펙을 검증하는데 최적화 되어 있음
    • 테스트 클래스 선언부에 붙이고 어떤 컨트롤러를 테스트할지 명시적으로 지정함
    • @WebMvcTest(AuthController.class) class AuthControllerTest { ... }
  3. @WithMockUser
    • Spring Security 테스트 지원 어노테이션
    • 테스트실행 시 가짜 로그인 사용자를 만들어서 컨트롤러(혹은 서비스)에서 인증/권한이 필요한 API도 로그인 상태로 테스트 가능
    • 기본값은 username : “user”, ROLE : “USER” → @WithMockUser(username="admin", roles={"ADMIN"}) 커스텀 가능
@WithMockUser(username = "admin", roles = {"ADMIN"}) @Test void 인증_필요_API_테스트() { ... }

 

[ 의존성 관련 어노테이션 ]

  1. @Mock (Mockito)
    • 테스트 대상 클래스가 의존하는 객체(Service, Mapper, 외부 API 등)를 가짜(Mock) 객체로 만듦
    • 진짜 객체의 작/DB/네트워크 호출없이, 원하는 동작/결과를 미리 세팅해서 단위 테스트, 로직 검증에만 집중할 수 있게 해줌
    • @Mock 은 테스트 클래스 필드에 선언함
    • Controller 테스트의 경우 Service 를 @MockBean 으로 주입하는게 더 일반적임
  2. @InjectMocks (Mockito)
    • @Mock 으로 만든 Mock 객체들을 자동으로 주입해서 테스트 대상 클래스를 실제 인스턴스로 만듦
    • 테스트 대상 객체(Service 등) 가 내부적으로 의존성을 주입받아야 할 때 Mock 객체를 알아서 필드/생성자에 할당해줌
    • @InjectMocks 는 테스트 클래스(테스트 대상)에 선언함
  3. @MockitoBean (Spring Core)
    • Spring Test 공식 제공 → Spring 6.x 공식 지원
    • 테스트 컨텍스트에 등록된 실제 빈을 Mock 객체로 대체
    • @MockBean(Spring boot) 대신 사용하는 최신방식
    • 주로 통합 테스트, 컨트롤러 테스트 등에서 Spring 컨테이너의 진짜 빈을 Mock 으로 바꿔서 주입
  4. @TempDir (JUnit)
    • 테스트 실행 중에 임시 디렉터리/파일 이 필요할 때 JUnit 이 자동으로 임시 폴더를 생성, 테스트가 종료되면 자동으로 정리
    • 파일 업로드/다운로드 등 파일 시스템 관련 테스트에서 사용
  5. @Autowired ObjectMapper, MockMvc
    • ObjectMapper : JSON 변환(직렬화/역직렬화) 지원
    • MockMvc : Spring MVC 환경에서 HTTP 요청/응답 테스트 지원
@Test @DisplayName("공지사항 리스트 조회 : 실패 - 잘못된 페이지 요청") 
void 실패_잘못된_페이지_요청() throws Exception { 
	// when & then 
    mockMvc.perform(get(NOTICE_LIST_API_URL)
    	.param("page", "0")
        .param("size", "1000") // 잘못된 페이지 사이즈 
        .param("keyword", "")
        .with(csrf().asHeader()))
      .andExpect(status().isBadRequest()) // 400 에러 검증
      .andExpect(jsonPath("$.message")
      .value(ErrorCode.INVALID_DATA_REQUEST.getMessage())); // CustomException -> INVALID_DATA_REQUEST 
    then(noticeService).should(never()).getNoticeList(any()); // 공지사항 리스트 조회 메서드 호출 검증
}

 

[ 네이밍 및 그룹핑에 사용된 어노테이션 ]

  1. @Nested
    • Junit5 에서 지원하는 기능으로 한 클래스 안에서 관련된 테스트 케이스끼리 내부 클래스로 논리적 그룹으로 묶어줌
    • 여러 기능/상황 등을 각각 내부 클래스로 구분함 IDE 트리에서도 그룹핑 됨
  2. @DisplayName
    • 각 테스트(@Test, @Nested, @TestClass) 에 사람이 읽기 쉬운 제목/설명을 붙임
    • 한글로 써도 되고 @Nested 에 @DisPlayName 지정 시 하위 테스트 클래스에서 해당 이름 인식
    • IDE, CI/CD 테스트 리포트에 트리뷰로 정리되어 노출
  3. @Test
    • JUnit 이 테스트 메서드로 인식하도록 함
    • 자동으로 빌드/테스트 툴에서의 실행과 성공/실패 여부가 CI/CD 리포트. IDE 에서 자동 체크 됨

'Project > Backend' 카테고리의 다른 글

[Groupware] Spring Security + Exception  (1) 2025.06.18
[Groupware] Spring-Boot : Spring Security + JWT  (2) 2025.06.16
[Groupware] Spring-Boot : API 공통 응답 객체  (1) 2025.05.30
[Groupware] Spring-Boot : Swagger 연동  (0) 2025.05.29
'Project/Backend' 카테고리의 다른 글
  • [Groupware] Spring Security + Exception
  • [Groupware] Spring-Boot : Spring Security + JWT
  • [Groupware] Spring-Boot : API 공통 응답 객체
  • [Groupware] Spring-Boot : Swagger 연동
arraysort
arraysort
arraysort 님의 블로그 입니다.
  • arraysort
    arraysort 님의 블로그
    arraysort
  • 전체
    오늘
    어제
    • 분류 전체보기 (14)
      • Study (5)
        • Java (3)
        • DataBase (1)
        • Spring-Boot (1)
        • React (0)
        • WEB (0)
      • Project (9)
        • Backend (5)
        • Frontend (2)
        • Database (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    SQL
    oracle
    backend
    utilityclass
    objects.eqauls()
    react
    FilterChain
    mabatis
    spring boot
    TypeScript
    VO
    API
    CDB
    Spring Security
    DTO
    lombok
    java
    Database
    Groupware
    SQL Mapper
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
arraysort
[Groupware] Spring-Boot : 테스트 코드
상단으로

티스토리툴바