이전 포스팅에서 테스트의 개념 및 종류와 단위 테스트&JUnit을 간략하게 다뤄보았다.
이번 포스팅에서는 스프링의 계층별 단위 테스트에 대해서 다뤄볼 예정이다.
[SpringBoot] 테스트 개념과 종류
🌱 테스트란? 테스트란 개발자가 작성한 코드가 의도된 대로 정확히 작동하는지 검증하는 절차이다. 🌱 테스트 코드를 작성해야 하는 이유 개발 과정 중 예상치 못한 문제를 미리 발견할 수 있
caffeineoverflow.tistory.com
[SpringBoot] 단위테스트(Unit Test)와 JUnit
🌱 단위 테스트(Unit Test)란? 단위 테스트(Unit Test)란 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트이다. 여기서 모듈은 애플리케이션에서 작동하는 하나의 기능 또는 메
caffeineoverflow.tistory.com
🌱 Controller, Service, Repository 구성
테스트에 앞서 기본이 되는 코드를 살펴보자.
회원을 조회하는 클라이언트의 요청이 들어오면, Controller -> Service -> Repository의 순으로 호출이 된다고 가정해 보자.
각 각의 코드는 최소한의 기능만 동작하게 구현하였다.
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
/**
* 회원 조회
*/
@GetMapping("/members/{memberId}")
public Member findMember(@PathVariable Long memberId) {
return memberService.findMember(memberId);
}
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Member findMember(Long memberId) {
return memberRepository.findByMemberId(memberId);
}
}
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByMemberId(Long memberId);
}
위의 코드를 기반으로 호출 역순인 Repository -> Service -> Controller 순으로 테스트 코드를 작성해 보자.
🌱 Repository 테스트
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
@DataJpaTest
class RepositoryUnitTest {
@Autowired
private MemberRepository memberRepository;
@Test
@DisplayName("회원 조회")
void findMember() {
// given
Member member = Member.builder()
.name("CHOI")
.build();
Member savedMember = memberRepository.save(member);
// when
Member findMember = memberRepository.findByMemberId(savedMember.getMemberId());
// then
Assertions.assertSame(savedMember, findMember);
Assertions.assertEquals(savedMember.getMemberId(), findMember.getMemberId());
Assertions.assertEquals(savedMember.getName(), findMember.getName());
}
}
각 각의 단위 테스트 코드를 보면 주석으로 given, when, then이라고 작성된 것을 볼 수 있을 것이다.
테스트 코드를 작성하는 패턴 중 하나로 Given-When-Then 패턴이라고 불린다.
각 각이 의미하는 바는 아래와 같다.
- Given
- 시나리오 진행에 필요한 값을 설정한다.
- Member객체를 생성한 후 저장하는 단계에 해당한다. - When
- 테스트하고자 하는 행동을 명시한다.
- 테스트 목적을 알 수 있는 영역이다. 여기에서는 저장된 Member 객체의 ID를 토대로 Member를 조회한다. - Then
- 테스트를 통해 도출된 결과를 검증한다.
- 저장을 위해 만든 Member객체(savedMember)의 ID와 이름이 조회를 통해 가져온 Member객체(findMember)의 ID와 이름과 동일한지 검증한다.
Given-When-Then 패턴에 대해서 간략하게 알아보았고, 다시 본론으로 돌아와 Repository 단위 테스트 코드에 대해서 알아보자.
DB 접근을 위해 JPA를 사용하였으며, JPA를 테스트하기 위해서 @DataJpaTest 어노테이션을 사용하였다.
@DataJpaTest 어노테이션은 JPA 관련 테스트 설정만 로드하며, 실제 데이터베이스가 아닌 내장 데이터베이스를 사용한다는 특징이 있다. 또한 기본적으로 @Transactional 어노테이션을 포함하고 있기 때문에 테스트가 완료되면 자동으로 롤백이 된다는 장점도 가지고 있다.
🌱 Service 테스트
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class ServiceUnitTest {
@InjectMocks
private MemberService memberService;
@Mock
private MemberRepository memberRepository;
@Test
@DisplayName("회원 조회")
void findMember() {
// given
Long fakeMemberId = 1L;
Member member = Member.builder()
.memberId(fakeMemberId)
.name("CHOI")
.build();
given(memberRepository.findByMemberId(fakeMemberId)).willReturn(member); // mocking
// when
Member findMember = memberService.findMember(fakeMemberId);
// then
Assertions.assertSame(member, findMember);
Assertions.assertEquals(member.getMemberId(), findMember.getMemberId());
Assertions.assertEquals(member.getName(), findMember.getName());
}
}
Service 계층의 단위 테스트는 앞서 살펴본 Repository 계층보다 살짝 복잡하다.
들어가기 전에 Mock 개념에 대해서 간단히 알아보도록 하자.
Mock이란 실제 객체를 만들어 사용하기에 시간, 비용 등의 Cost가 높거나 혹은 객체 서로 간의 의존성이 강해 구현하기 힘들 경우 가짜 객체를 만들어 사용하는 방법이다. 이러한 가짜(Mock) 객체를 만들 수 있도록 지원하는 프레임워크 중 Mockito라는 테스트 프레임워크가 있다.
Service 계층에서는 이 Mockito를 이용하여 단위 테스트를 진행하였으며, 사용된 어노테이션 및 용어에 대해서 조금 더 알아보도록 하자.
- @ExtendWith
단위 테스트에 공통적으로 사용할 확장 기능을 선언해 주는 역할을 한다. SpringExtension.class 또는 MockitoExtension.class를 많이 사용한다. Spring Test Context 프레임워크와 Junit5와 통합해 사용할 때는 SpringExtension.class을, JUnit5와 Mockito를 연동해 테스트를 진행할 경우에는 MockitoExtension.class를 사용한다. - @Mock
Mock 객체를 생성한다. 실제로 메서드는 갖고 있지만 내부 구현이 없는 상태이다. - @Spy
모든 기능을 가지고 있는 완전한 객체이다. Stub 하지 않은 메서드들은 원본 메서드 그대로 사용한다. 즉, 테스트 대상의 일부분만 Mocking 하는 것이다. 대체로 @Spy보다는 @Mock을 쓰는 것을 추천하지만, 외부 라이브러리를 이용한 테스트에는 @Spy를 사용하는 것을 추천한다. - @InjectMocks
@Mock 또는 @Spy로 생성된 가짜 객체를 자동으로 주입시켜 주는 객체이다.
@InjectMocks 객체에서 사용할 객체를 @Mock으로 만들어 쓰면 된다. 만약 Service를 테스트하는 클래스를 생성했다면, Service 객체를 @InjectMocks 어노테이션을 사용해 생성하고, Service단에서 사용할 Repository와 같은 객체들은 @Mock 어노테이션을 사용해 생성하면 된다. - Stub
다른 객체 대신에 가짜 객체(Mock Object)를 주입하여 어떤 결과를 반환하라고 정해진 답변을 준비시킨다. Mock 객체의 메서드를 호출해도 실제로 코드를 실행하지 않기 때문에, 메서드의 행동을 미리 정해두어야 한다.
해당 개념을 기반으로 테스트 코드를 분석해 보자면, 전체적인 흐름(=사용자 저장 후 조회하여 검증)은 Repository와 유사한 흐름이다.
다만 given(memberRepository.findByMemberId(fakeMemberId)).willReturn(member); 코드가 새롭게 추가된 것을 볼 수 있을 것이다. 이는 바로 Mock 개념이 들어간 코드인데, 그중에서도 앞서 살펴본 Stub의 개념이 녹아든 코드이다.
memberRepository는 @Mock 어노테이션이 붙은 걸로 보았을 때, 내부 구현이 없는 상태라는 것을 유추할 수 있을 것이다.
즉, membserService.findMember()가 호출되면 다시 memberRepository.findByMemberId()가 호출되는 것인데 내부 구현이 없는 메서드를 호출하는 것과 같아지게 된다.
따라서 'memberRepository.findByMemberId(fakeMemberId) 호출되면 member를 반환하는 행동'을 미리 지정한 것이 given(memberRepository.findByMemberId(fakeMemberId)).willReturn(member); 코드인 셈인 것이다.
다시 말해서, 위의 코드는 Service계층에 의해 memberRepository.findByMemberId()가 호출되면 member 객체를 그대로 반환하도록 지정하는 코드로 이해를 하면 될 것이다.
🌱 Controller 테스트
import jakarta.ws.rs.core.MediaType;
import org.junit.jupiter.api.DisplayName;
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.boot.test.mock.mockito.MockBean;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
class ControllerUnitTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MemberService memberService;
@Test
@DisplayName("회원 조회")
void findMember() throws Exception {
// given
Long fakeMemberId = 1L;
Member member = Member.builder()
.memberId(1L)
.name("CHOI")
.build();
given(memberService.findMember(fakeMemberId)).willReturn(member); // mocking
// when
String memberId = "1";
ResultActions actions = mockMvc.perform(
get("/members/{memberId}", memberId)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
);
// then
actions.andExpect(status().isOk())
.andExpect(jsonPath("$.memberId").value(1))
.andExpect(jsonPath("$.name").value("CHOI"))
.andDo(print());
}
}
Controller를 테스트하기 위해서는 HTTP 호출이 필요하다. 일반적인 방법으로는 HTTP 호출이 불가능하므로 스프링에서는 이를 위한 MockMvc를 제공하고 있다. MockMvc에서 제공하는 주요 메서드도 알아보도록 하자.
- perform()
어떤 요청인지, 요청에서 바디에 담긴 타입과 내용들은 무엇인지, 요청에 대한 정보들을 설정한다. - get()
Http method와 동일하게 get 뿐만 아니라 post(), put()등도 존재하며, 호출할 URI를 설정한다. - contentType()
바디에 담길 내용의 타입을 설정한다. - andExpect()
요청을 보낸 후 응답에 대해 검증하는 메서드이다. - status().isOk()
응답이 성공적으로 되었는지 체크하는 메서드이다. HTTP 응답값을 확인할 수 있는 용도이다. - jsonPath
응답에 대한 값을 확인할 수 있는 메서드이며, 표현식을 통해 json의 Key를 가져와 값을 비교할 수 있다.
References.
1. 고딩왕 코범석 - JUnit과 계층별 단위 테스트 정리
2. yyong3519 - JUnit 과 Mockito 기반의 단위 테스트 코드 작성
3. u-nij - [JUnit5] 단위 테스트(@Extendwith, Mockito)
4. 망나니개발자 - [Java] JUnit을 활용한 Java 단위 테스트 코드 작성법 (3)
'Spring' 카테고리의 다른 글
[Spring] 어노테이션(Annotation) (0) | 2023.11.17 |
---|---|
[SpringBoot] 통합테스트(Integration Test) (0) | 2023.11.13 |
[Spring] @Configuration (0) | 2023.10.06 |
[SpringBoot] 단위테스트(Unit Test)와 JUnit (0) | 2023.09.30 |
[SpringBoot] 테스트 개념과 종류 (0) | 2023.09.28 |