Develop/Spring

테스트 코드

mabubsoragodong 2025. 3. 12. 23:27

테스트 코드 작성 전 알고 있어야 할 사항

테스트코드는 어떻게 구성해야 할까?

보통 이렇게 구성하는 것이 일반적이다.

  1. 정상 케이스
    • 입력값의 범위가 0~128 일때 10, 50, 100 등을 입력 받았을때 테스트
  2. 엣지 케이스(경계 조건)
    • 입력값의 범위가 10미만일 때 A동작, 10이상 일때 B동작을 해야한다면 9, 10, 11 에 대한 테스트
  3. 예외 상황
    • 입력값이 숫자만 받아야할때 문자를 입력한 경우 예외처리가 되는지 테스트

정상의 경우, 경계값, 예외상황 이렇게 3가지를 염두해 두고 작성하자.

 

테스트코드 준비

스프링부트에서 제공하는 테스트 라이브러리를 의존성에 추가

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

H2 (Repository 테스트 or 통합 테스트용) 데이터베이스 의존성을 추가

dependencies {
    testRuntimeOnly 'com.h2database:h2'
}

 

properties 설정 (문제 발생)

기본적으로 테스트코드는 h2 데이터베이스를 사용해준다. 다른 데이터베이스를 사용하게 된다면 프로퍼티에 설정해주어야한다.

그럼 프로퍼티 파일을 어떻게 나누나??

 

일단 우리가 아는 .properties 파일은 main/resources/application.properties 파일이다. 테스트코드의 properties 파일은 우선적으로 test/resources/프로퍼티파일 을 읽게된다. 따라서 해당 위치에 프로퍼티 파일을 작성해주면 된다.

 

이렇게 파일을 작성해주고 test/resouces/application.properties 파일안에 아무것도 작성해주지 않아도 default로 h2 데이터베이스가 동작하기 때문에(h2 의존성 추가해주어서) repository test code 실행이 가능하다. 

 

그런데 여기서 무슨 문제가 발생했느냐?

나는 application-test.properties 이름으로 프로퍼티 파일을 생성했고 해당 파일을 main/resources에 두어도, test/resources 에 두어도 테이블을 찾지 못했다는 오류가 발생했다. 

 

그래서 자료를 찾아보던 중 테스트 코드는 test 하위의 application.properties 파일을 읽는 다는 것을 알게 되었고 이름을 바꿔서 test/resources/application.properties 와 같이 위치 시키니 에러가 발생하지 않았다. 

 

하지만, 본래 생각했던 문제 해결책은

 

application-test.properties와 같이 이름을 지어서 main 밑에 두고, 해당 파일에 spring.config.activate.on-profile=test 를 작성해 이 프로퍼티 파일 프로필을 test와 같이 하겠다.로 설정하였다. 

그리고 테스트코드 클래스에 @ActiveProfiles("test") 어노테이션을 달아두었다. 그럼 테스트코드가 main 밑의 application-test.properties를 읽어와서 적용할 줄 알았다.

 

그런데 그렇게 되지 않았다.... 아 그럼 application-test.properties를 main 밑에 두어서 그런가? 하고 test 밑에 두고 다시 실행해보았지만 되지 않았다. 

 

오로지 test 하위에 application.properties 와 같이 파일이름을 작성해야지만 제대로된 프로퍼티 파일을 불러왔다. 

 

내 추측으로는 main, test 둘 다 기본적으로 우선으로 읽어들이는 파일은 application.properties의 이름으로 설정된 파일인 것 같다.

 

하지만 튜터님과 상의한 결과 나중에 배포를 위해서라도 프로퍼티 파일을 분리해서 사용하게 될텐데, 프로퍼티 파일이름을 분리해서 사용하는 방법을 익혀두어야 할 것 같다.  

 

given when then 패턴

 

스프링에서 테스트 코드를 짤 때 사용하는 어노테이션

1. @DataJpaTest

  • JPA와 관련된 Component들만 모두 가지고와 Repository Layer 단위 테스트를 위한 Annotation이다.
  • 기본적으로 In-memory DB(ex H2)를 사용하여 테스팅

요약: JPA 레포지토리 단위 테스트

2. @ExtendWith

  • Junit5 환경에서 확장기능을 사용할때 사용.
  • 주로 MockitoExtension 과 함께 Service Layer, 클래스 단위 테스트에 사용

요약: 서비스 단위 테스트

3. @WebMvcTest

  • 스프링의 Web Layer(controller, filter 등)을 테스트하기 위한 Annotation이다.
  • @Controller, @ControllerAdvice 등 웹과 관련된 Bean만 로드하여 테스트를 수행한다.

요약: 컨트롤러 단위 테스트

4. @SpringBootTest

  • 스프링 부트 전체를 테스트 수행하기 위해서 사용하는 Annotation이다.
  • 서버를 실행하듯 모든 스프링 Context를 로드한다.

요약: 통합 테스트

 

Mocking이 뭐야?

테스트 코드를 작성하면 테스트를 하기 위한 코드 이외의 의존 객체들이 존재하는 경우가 있다.

예를 들면 Service 코드를 테스트 하는데 Repository가 필요한 경우이죠. 그럴 때 사용하는 가짜 객체가 Mock이다. 

 

사용 이유

  1. 외부 의존성 제거
  2. 테스트 범위 준수
  3. 에러 상황 강제 발생

서비스 테스트

서비스 테스트를 하려면 먼저 테스트 클래스 위에 @ExtendWith(MockitoExtension.class) 어노테이션을 달아준다.

    @Test
    void 회원가입을_정상적으로_할_수_있다() {
        //given
        String username = "yubin";
        String email = "test@test.com";
        String password = "123456";
        Member member = new Member(username, email, password);

        // `memberRepository.save(member)`가 호출되면 save의 인자로 어떤
        // member 객체가 오더라도 member 객체를 반환하도록 설정
        when(memberRepository.save(any(Member.class))).thenReturn(member);

        // when
        memberService.signup(username, email, password);

        // then
        verify(memberRepository).save(any(Member.class)); // signup()을 하면 save()가 호출되었는지 검증

        System.out.println("\n테스트 성공!!!\n");
    }

 

서비스 클래스는 데이터베이스를 사용하지 않는다. 따라서 Mock 객체를 만들어주어야 한다.

  • @Mock: 테스트 대상 외의 의존 객체
  • @InjectMocks: 테스트 대상 객체
    @Mock
    private MemberRepository memberRepository;

    @InjectMocks
    private MemberService memberService;

 

하지만 @Mock으로 memberRepository를 설정해주었다고 해서 memberRepository.save()해서 데이터베이스에 저장되었겠지? 하고 findById같은 거 하면 null이 반환된다. 그냥 의존성이 있는데 필요하니 가져올 수 있도록 해준걸로 생각하자.

 

@Test
    void 회원가입을_정상적으로_할_수_있다() {
        //given
        String username = "yubin";
        String email = "test@test.com";
        String password = "123456";
        Member member = new Member(username, email, password);

        // `memberRepository.save(member)`가 호출되면 save의 인자로 어떤
        // member 객체가 오더라도 member 객체를 반환하도록 설정
        when(memberRepository.save(any(Member.class))).thenReturn(member);

        // when
        memberService.signup(username, email, password);

        // then
        verify(memberRepository).save(any(Member.class)); // signup()을 하면 save()가 호출되었는지 검증

    }

 

    @Test
    void 존재하지_않는_Member를_삭제_시_예외가_발생한다() {

        // given
        Long userId = 1l;
        when(memberRepository.findById(userId)).thenReturn(null);

        // when & then
        assertThrows(RuntimeException.class, () -> memberService.deleteMember(userId));

    }

 

given().willReturn() 과 when().thenReturn()이 무슨 차이이고 어떻게 동작하는가?

 

둘 다 같은 동작을 하지만, BDDMockito 스타일을 적용한 것이 given().willReturn()이다.

  • when().thenReturn()은 일반적인 Mockito 방식
  • given().willReturn()은 BDDMockito 스타일

 

컨트롤러 테스트

Controller 테스트는 스프링 부트 3.4.x부터 업데이트 사항이 존재합니다. 이에 유의해주세요!

 

@MockBean: Bean을 Mocking(3.4.x 이전. 3.4.x부터는 deprecated되어 사용 불가)

@MockitoBean: Bean을 Mocking(3.4.x 이후)

 

MockMvc: 스프링 MVC 컨트롤러를 테스트할 수 있게 해주는 모의 HTTP 요청/응답 도구

MockMvcTester: MockMvc의 래핑(상위 추상화) 버전, AssertJ와 더 잘 통합될 수 있음**(3.4.x 이후)*

 

한동안 @MockitoBean과 MockMvcTester를 사용할 일은 아직 없습니다.

다만,  직접 생성한 프로젝트는 3.4.x버전이므로 @MockitoBean과 MockMvc를 활용한다.

 

package com.example.testcode2;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.awaitility.Awaitility.given;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.doNothing;

import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.mockito.Mockito.doNothing;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;

@WebMvcTest(MemberController.class)
class MemberControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private MemberService memberService;
    @Autowired
    private ObjectMapper jacksonObjectMapper;

    @Test
    void 회원가입() throws Exception {
        // given
        SignupDto signupDto = new SignupDto("yubin", "test@test.com", "123456");
        String requestBody = jacksonObjectMapper.writeValueAsString(signupDto);

        doNothing().when(memberService).signup(signupDto.getUsername(), signupDto.getEmail(), signupDto.getPassword());

        // when & then
        mockMvc.perform(post("/signup")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(requestBody))
                .andExpect(status().isOk())
                .andExpect(content().string("Signup successful"));

    }

}

 

 

SignupDto 객체를 생성하고, ObjectMapper를 이용해 JSON으로 변환.

memberService.signup() 메서드는 실제 실행하지 않고, doNothing()으로 처리.

MockMvc를 사용하여 /signup API 호출.

응답이 200 OK이고, 본문이 "Signup successful"인지 검증.

 

 

리포지토리 테스트

리포지토리 테스트를 하려면 먼저 테스트 클래스 위에 @DataJpaTest를 붙여준다.

그리고 UserRepository를 사용할 것이기 때문에 필드에 작성해주고 @Autowired를 붙여준다.

 

테스트할 메서드를 고르고 어떠한 테스트를 할 것인지 테스트메서드 명을 작성해준다.

이 메서드를 테스트 해보겠다. 

Optional<Member> findByEmail(String email);

 

given when then 패턴 틀을 만들어주자.

    @Test
    void 이메일로_사용자를_조회_할_수_있다() {
        //given

        //when

        //then
    }

 

이메일로 사용자 조회하려면 일단 무엇이 필요한가?

사용자가 필요하다. 사용자를 만들어서 디비에 저장되어 있는 상태를 만들어주어야 한다.

//given
String username = "yubin";
String email = "test@test.com";
String password = "123456";
Member member = new Member(username, email, password);
memberRepository.save(member);

 

그리고 "이메일로 사용자를 조회할 때"를 테스트해야한다.

//when
Member foundMember = memberRepository.findByEmail(email).orElse(null);

 

그리고 ~~와 같이 될것이다.로 assert한다.

assertNotNull(foundMember);
assertEquals(email, foundMember.getEmail());

테스트가 성공적으로 실행된다면 foundMember는 null이 아닐것이다라는 의미이다. 또한 테스트 given 값으로 넣어준 email 값과 리포지토리 메서드를 통해 찾은 객체의 email 값이 같을 것이다라는 의미이다.