테스트 코드
테스트 코드 작성 전 알고 있어야 할 사항
테스트코드는 어떻게 구성해야 할까?
보통 이렇게 구성하는 것이 일반적이다.
- 정상 케이스
- 입력값의 범위가 0~128 일때 10, 50, 100 등을 입력 받았을때 테스트
- 엣지 케이스(경계 조건)
- 입력값의 범위가 10미만일 때 A동작, 10이상 일때 B동작을 해야한다면 9, 10, 11 에 대한 테스트
- 예외 상황
- 입력값이 숫자만 받아야할때 문자를 입력한 경우 예외처리가 되는지 테스트
정상의 경우, 경계값, 예외상황 이렇게 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이다.
사용 이유
- 외부 의존성 제거
- 테스트 범위 준수
- 에러 상황 강제 발생
서비스 테스트
서비스 테스트를 하려면 먼저 테스트 클래스 위에 @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 값이 같을 것이다라는 의미이다.