슬라이스 테스트
슬라이스라는 단어처럼 자기가 테스트하고자 하는 계층 외에
나머지 계층들과는 단절하여 오로지 하나의 계층만 테스트하는 방법이다.
컨트롤러 슬라이스 테스트
서비스 영역 같은 다른 영역을 사용하지 않고 컨트롤러만 사용하여
테스트를 하고자 하는 경우에는 기본적인 양식은 아래와 같다.
@WebMvcTest(AnswerController.class)
public class QuestionControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private Gson gson;
@MockBean
private QuestionService questionService;
@MockBean
private QuestionMapper questionMapper;
// 테스트 코드들
}
@WebMvcTest(테스트하려는 컨트롤러의 클래스)
해당 어노테이션을 사용하면 컨트롤러 테스트에 필요한 빈만 사용하여 테스트를 수행한다.
@SpringBootTest 어노테이션은 등록된 모든 빈을 사용하여 테스트를 수행하기 때문에
속도적인 측면에서 슬라이스 테스트에 사용하긴 효율적이지 못하다.
MockMvc
컨트롤러를 테스트하기 위해 API를 호출하고 검증하는 기능을 제공한다.
Gson
객체를 json 형식으로 바꿀 때 사용한다. (json 형식 이외에도 다른 형식으로도 바꿀 수 있다.)
@MockBean
테스트에 사용할 목 객체(가짜 객체)를 빈으로 등록하여 실제 빈 대신에 동작하게 한다.
스프링 테스트 프레임워크와 함께 쓰이며 통합 테스트에서 스프링 빈을 목 객체로 대체하려 사용한다.
@MockBean과 @Mock
두 어노테이션 모두 테스트 과정에 사용 할 목객체를 생성하고 대체하는 것은 똑같지만
어떤 프레임워크와 같이 사용되고 어느 테스트에 사용되는지가 다르다.
@MockBean 어노테이션은 위에서 언급했듯이 스프링 테스트 프레임워크와 함께 사용하여
주로 통합 테스트에 사용된다.
@Mock 어노테이션은 Mockito 프레임워크와 함께 사용되어 주로 단위 테스트에 사용된다.
컨트롤러 테스트에서는 스프링에서 제공하는 WebMvcTest를 사용하기 때문에
@MockBean 어노테이션을 사용한다.
@Test
public void postQuestionTest() throws Exception {
long memberId = 1L;
Member member = new Member();
member.setMemberId(memberId);
Question question = new Question();
question.setQuestionTitle("Question Title");
question.setQuestionText("Question Text");
question.setQuestionStatus(Question.QuestionStatus.QUESTION_REGISTERED);
question.setQuestionOpen(Question.QuestionOpen.SECRET);
question.setMember(member);
QuestionDto.Post postDto = new QuestionDto.Post(
"Question Title",
"Question Text",
Question.QuestionOpen.PUBLIC
);
String requestContent = gson.toJson(postDto);
given(questionMapper.questionPostToQuestion(Mockito.any(QuestionDto.Post.class)))
.willReturn(new Question());
given(questionService.createQuestion(Mockito.any(Question.class), Mockito.anyLong()))
.willReturn(question);
mockMvc.perform(
post(root + "/create")
.param("memberId", String.valueOf(memberId))
.contentType(MediaType.APPLICATION_JSON)
.content(requestContent)
)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));
}
POST 컨트롤러의 간단한 테스트 코드를 나눠서 살펴보겠다.
long memberId = 1L;
Member member = new Member();
member.setMemberId(memberId);
Question question = new Question();
question.setQuestionTitle("Question Title");
question.setQuestionText("Question Text");
question.setQuestionStatus(Question.QuestionStatus.QUESTION_REGISTERED);
question.setQuestionOpen(Question.QuestionOpen.SECRET);
question.setMember(member);
given에 사용될 데이터를 미리 하나 만들어둔다.
given 부분을 설명할 때 다시 설명하겠다.
QuestionDto.Post postDto = new QuestionDto.Post(
"Question Title",
"Question Text",
Question.QuestionOpen.PUBLIC
);
String requestContent = gson.toJson(postDto);
실제 클라이언트가 요청에 사용할 DTO 객체를 하나 만든 후에
해당 객체를 Gson을 사용해 json 형태로 변환한다.
given(questionMapper.questionPostToQuestion(Mockito.any(QuestionDto.Post.class)))
.willReturn(new Question());
QuestionMapper와 QuestionService 객체들은 컨트롤러만 슬라이스 테스트 하기 위해서는
불필요한 객체들이기 때문에 어떤 값을 전달 받아서 실행이 되어도
항상 정해진 결과를 반환하게 고정한다.
문법이 어렵게 느껴질 수도 있지만 코드를 읽어 보면 어렵지 않다.
QuestionMapper 객체의 questionPostToQuestion 메서드에
어떤 QuestionDto.Post 타입의 객체가 전달되어도
항상 Question 객체를 반환하겠다는 것이다.
given(questionService.createQuestion(Mockito.any(Question.class), Mockito.anyLong()))
.willReturn(question);
첫 given에서는 직접 정의해둔 결과가 아닌 임의의 새로운 질문 객체를 리턴하도록 지정했지만
마지막 given에서는 직접 정의해둔 객체를 리턴하도록 지정했다.
해당 컨트롤러에서는 QuestionService 객체의 createQuestion 메서드를 호출한
결과값이 리턴값이기 때문에 결과를 직접 정의한 객체로 정해줬다.
컨트롤러의 테스트는 결국에 클라이언트의 요청과 서버의 응답만 테스트하는 것이
목적이기 때문에 응답에 영향을 미치지 않는 중간 과정들은 어떤 값을 리턴하든 상관 없고
최종적인 결과에 영향을 미치는 과정만 리턴 값을 정의해주면 된다.
mockMvc.perform(
post(root + "/create")
.param("memberId", String.valueOf(memberId))
.contentType(MediaType.APPLICATION_JSON)
.content(requestContent)
)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));
지금까지는 테스트를 수행하기 위한 준비 과정이었다면
이제 MockMvc의 perform 메서드를 사용해 컨트롤러 테스트를 할 수 있다.
post(root + "/create")
위의 코드처럼 어떤 종류의 요청을 테스트할지 정한 후에
해당 요청을 받을 주소를 설정해준다.
.param("memberId", String.valueOf(memberId))
.contentType(MediaType.APPLICATION_JSON)
.content(requestContent)
해당 요청에 대한 추가적인 정보들을 지정해주면 요청에 대한 응답 데이터를 담은
ResultActions 객체가 반환된다.
위의 코드에서는 주소의 파라미터 값(?memberId=1)을 param 메서드를 사용해 지정해주고
기존에 Gson을 사용해 json 형식으로 만들어두었던 요청 데이터를
클라이언트가 요청을 보낼 때 사용할 데이터로 지정해준다.
.andExpect(status().isCreated())
.andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));
ResultActions 객체가 가지고 있는 데이터를 사용해
서비스 슬라이스 테스트
컨트롤러 슬라이스와 마찬가지로 서비스도 서비스가 의존하고 있는 객체과의
관계를 끊고 목 객체를 이용하여 테스트하면 된다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
위와 같은 구조의 서비스 클래스를 테스트 해보자
@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {
@Mock
private MemberRepository memberRepository;
@InjectMocks
private MemberService memberService;
// 테스트 코드 생략
}
서비스 슬라이스 테스트는 기본적으로 위와 같은 양식을 가진다.
@ExtendWith(추가할 확장 클래스)
JUnit5의 확장 모델을 사용하기 위해 사용하는 어노테이션이다.
현재 테스트 클래스에서는 Mockito 확장을 추가하기 위해 사용하였다.
@Mock
컨트롤러 테스트에서 알아봤듯이 Mockito 프레임워크를 사용할 때
목 객체를 생성하기 위한 어노테이션이다.
@InjectMocks
@Mock 어노테이션이 적용된 객체들을 해당 어노테이션이 적용된 객체에 주입한다.
서비스 클래스에서 의존관계를 주입하는 것과 같은 작업이라고 볼 수 있다.
@Test
public void saveMemberTest() throws Exception{
Member member = new Member();
member.setMemberName("이름");
member.setMemberId(1L);
member.setMemberEmail("test@email.com");
given(memberRepository.save(Mockito.any(Member.class))).willReturn(member);
assertThat(
memberService.createMember(member).getMemberName(),
is(equalTo(member.getMemberName()))
);
}
테스트 하는 방식은 컨트롤러와 다를게 없다.
'Back-End > Spring' 카테고리의 다른 글
[로깅] MDC를 이용한 식별된 로깅 처리 (1) | 2024.03.05 |
---|---|
[JPA] N + 1 문제와 해결 방법 (1) | 2024.03.01 |
예외 (0) | 2023.06.15 |
템플릿 정리 (0) | 2023.06.14 |
JDBC Template (0) | 2023.06.13 |