4.슬라이스 테스트(Slice Test) |
-단위 테스트 만으로는 애플리케이션의 모든 기능이 정상적으로 동작한다고 보장되지 않음
-하나의 애플리케이션은 계층별로 역할이 있고, 계층별로 서로 연동되기 때문에 각각의 계층별로 잘 동작하는지 테스트한 후에 마지막으로 통합 테스트를 통해서 계층 간의 연동에 문제가 없는지 확인해야 테스트 작업이 마무리됨
-이 처럼 개발자가 각 계층에 구현해 놓은 기능들이 잘 동작하는지 특정 계층만 잘라서(Slice) 테스트하는 것을 슬라이스 테스트라고 함
+스모크 테스트(Smoke Test) : QA 부서에서 본격적으로 전체적인 기능 테스트를 진행하기 전에 애플리케이션의 특정 수정 사항으로 인해 영향을 받을 수 있는 범위에 제한해서 진행하는 테스트
4-1.API 계층 테스트
-API 계층 테스트 대상은 대부분 클라이언트의 요청을 받아들이는 핸들러인 Controller임.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest // (1)
@AutoConfigureMockMvc // (2)
public class ControllerTestDefaultStructure {
// (3)
@Autowired
private MockMvc mockMvc;
// (4)
@Test
public void postMemberTest() {
// given (5) 테스트용 request body 생성
// when (6) MockMvc 객체로 테스트 대상 Controller 호출
// then (7) Controller 핸들러 메서드에서 응답으로 수신한 HTTP Status 및 response body 검증
}
}
|
cs |
-Spring에서는 Controller를 테스트 하기 위해 아래와 같은 애너테이션을 제공함
- @SpringBootTest : Spring Boot 기반의 애플리케이션을 테스트 하기 위한 Application Context 생성
- @AutoConfigureMockMvc : 테스트에 필요한 애플리케이션의 구성이 자동으로 진행됨
- @Autowired : DI로 주입 받은 MockMvc는 Tomcat 같은 서버를 실행하지 않고 테스트 환경을 지원해줌
- @Test : 테스트 하고자 하는 Controller 핸들러 메서드의 테스트 케이스를 작성하면 됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
@Transactional
@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private Gson gson;
@Test
public void postMemberTest() throws Exception {
// given 테스트용 request body 생성
MemberDto.Post post = new MemberDto.Post( //(1)
"hgd@gmail.com",
"홍길동",
"010-1234-5678"
);
String content = gson.toJson(post); //(2)
// when MockMvc 객체로 테스트 대상 Controller 호출
ResultActions actions =
mockMvc.perform( //(3)
post("/v11/members") //(4)
.accept(MediaType.APPLICATION_JSON) //(5)
.contentType(MediaType.APPLICATION_JSON) //(6)
.content(content) //(7)
);
// then Controller 핸들러 메서드에서 응답으로 수신한 HTTP Status 및 response body 검증
actions //(8)
.andExpect(status().isCreated()) //(9)
.andExpect(header().string("Location", is(startsWith("/v11/members/")))); //(10)
}
@Test
void getMemberTest() throws Exception {
// ===================================postMember()를 이용한 테스트 데이터 생성 시작 //(11)
// given
MemberDto.Post post = new MemberDto.Post(
"hgd@gmail.com",
"홍길동",
"010-1111-1111"
);
String postContent = gson.toJson(post);
ResultActions postActions =
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(postContent)
);
// ===================================postMember()를 이용한 테스트 데이터 생성 끝
String location = postActions.andReturn().getResponse().getHeader("Location"); // = "/v11/members/1" //(12)
// when / then
mockMvc.perform(
get(location) //(13)
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.email").value(post.getEmail())) //(14)
.andExpect(jsonPath("$.data.name").value(post.getName()))
.andExpect(jsonPath("$.data.phone").value(post.getPhone()));
}
}
|
cs |
-위의 코드는 슬라이스 테스트 예시로 MemberController의 postMember() 핸들러 메서드와 getMember() 핸들러 메서드를 테스트하는 케이스임
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
● postMemberTest() 메서드
(1) MemberDto.Post 객체를 통해 Postman을 사용할 때 request body에 포함시키는 요청 데이터와 동일한 역할을 수행함
(2) Gson이라는 JSON 변환 라이브러리를 통해 (1)에서 생성한 MemberDto.Post 객체를 JSON 포맷으로 변환함
(3) MockMvc로 테스트 대상 Controller의 핸들러 메서드에 요청을 전송하기 위해 perform() 메서드를 호출함
(4) HTTP request에 대한 정보로, post() 메서드를 통해 HTTP POST METHOD와 request URL을 설정함
(5) HTTP request에 대한 정보로, accept() 메서드를 통해 클라이언트에서 리턴 받을 응답 데이터 타입을 JSON으로 설정
(6) HTTP request에 대한 정보로, contentType() 메서드를 통해 서버 쪽에서 처리 가능한 Content Type을 JSON으로 설정
(7) HTTP request에 대한 정보로, content() 메서드를 통해 request body 데이터를 설정함(Gson 라이브러리를 이용해 변환된 JSON 문자열)
(8) MockMvc의 perform() 메서드는 ResultAction 타입의 객체를 리턴하며, 이 객체를 이용해 전송한 request에 대한 검증을 수행함
(9) andExpect() 메서드를 통해 파라미터로 입력한 매처(Matcher)로 예상되는 기대 결과를 검증할 수 있으며, status().isCreated()를 통해 response status가 201(Created)인지 검증하고 있음.
(10) andExpect() 메서드와 header().string("Location", is(startsWith("/v11/members/"))) 을 통해 HTTP header에 추가된 Location의 문자열 값이 “/v11/members/”로 시작하는지 검증하고 있음
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
● getMemberTest() 메서드
(11) postMember() 를 통해서 테스트 데이터를 백엔드 서버 측의 데이터베이스에 저장함
(12) postActions.andReturn().getResponse().getHeader("Location") 을 통해 postMember()의 response에 전달되는 Location header 값을 가져올 수 있음.(위의 코드에서 location에 할당되는 값은 "/v11/members/1" )
(13) (12)에서 얻은 Location header 값을 get(location)으로 전달함(get("/v11/members/1" )과 같은 결과)
(14) andExpect() 메서드와 jsonPath() 메서드를 통해 response body(JSON 형식)의 각 프로퍼티 중에서 응답으로 전달 받는 email, name, phone 값이 request body로 전송한 email, name, phone과 일치하는지 검증하고 있음
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
※ 위의 예시 코드처럼 테스트를 진행할 경우 Controller만 테스트하는 것이 아니라 애플리케이션의 전체 로직을 모두 실행하게 되므로(API 계층을 테스트하려 했지만 서비스 계층과 데이터 액세스 계층의 로직까지 수행되었음) 완전한 슬라이스 테스트라고 볼 수 없음.
※※ 위의 문제는 Mock(가짜) 객체를 사용해 계층 간의 연결을 끊어줌으로써 해결할 수 있음
4-2.데이터 액세스 계층 테스트
-데이터 액세스 계층 테스트의 한 가지 규칙
● DB의 상태를 테스트 케이스 실행 이전으로 되돌려서 깨끗하게 만들기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@DataJpaTest // (1)
public class MemberRepositoryTest {
@Autowired
private MemberRepository memberRepository; // (2)
@Test
public void saveMemberTest() {
// given (3)
Member member = new Member();
member.setEmail("hgd@gmail.com");
member.setName("홍길동");
member.setPhone("010-1111-2222");
// when (4)
Member savedMember = memberRepository.save(member);
// then (5)
assertNotNull(savedMember); // (5-1)
assertTrue(member.getEmail().equals(savedMember.getEmail()));
assertTrue(member.getName().equals(savedMember.getName()));
assertTrue(member.getPhone().equals(savedMember.getPhone()));
}
@Test
public void findByEmailTest() {
// given (6)
Member member = new Member();
member.setEmail("hgd@gmail.com");
member.setName("홍길동");
member.setPhone("010-1111-2222");
// when
memberRepository.save(member); // (7)
Optional<Member> findMember = memberRepository.findByEmail(member.getEmail()); // (8)
// then (9)
assertTrue(findMember.isPresent()); // (9-1)
assertTrue(findMember.get().getEmail().equals(member.getEmail())); // (9-2)
}
}
|
cs |
-위의 코드는 슬라이스 테스트 예시로 MemberRepository의 saveMember() 핸들러 메서드와 findByEmail() 핸들러 메서드를 테스트하는 케이스임
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
● saveMemberTest() 메서드
(1) @DataJpaTest 애너테이션을 테스트 클래스에 추가하면 MemberRepository의 기능을 정상적으로 사용하기 위한 Configuration을 Spring이 자동으로 관리하며, @DataJpaTest 은 @Transactional 애너테이션을 포함하고 있기 때문에 하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 처리됨
(2) 테스트 대상 클래스인 MemberRepository를 DI 받음
(3) 테스트 할 회원 정보 데이터(member)를 준비함
(4) 회원 정보를 저장함
(5) 회원 정보가 잘 저장되었는지 검증함
(5-1) 회원 정보를 정상적으로 저장한 뒤에 리턴 값으로 반환 된 Member 객체(savedMember)가 null인지 검증하고 있음
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
● findByEmailTest() 메서드
(6) 테스트 할 회원 정보 데이터(member)를 준비함
(7) 회원 정보를 저장함
(8) (7)에서 저장한 회원 정보 중에서 이메일에 해당되는 회원 정보를 잘 조회하는지 테스트하기 위해 findByEmail()로 회원 정보를 조회하고 있음
(9) 회원 정보의 조회가 정상적으로 이루어지는지 검증함
(9-1) 조회된 회원 정보가 null이 아닌지를 검증하고 있음
(9-2) 조회한 회원의 이메일 주소와 테스트 데이터의 이메일과 일치하는지 검증하고 있음
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
+Spring JDBC 나 Spring Data JDBC 환경에서는 각각 @JdbcTest, @DataJdbcTest 애너테이션을 사용해 데이터 액세스 계층에 대한 테스트를 진행할 수 있음
5.Mockito |
-영어 사전에서 mock = ’not real, but intended to be very similar to a real situation, substance etc’
-테스트 세계에서의 Mock은 가짜 객체를 의미하며, 테스트에서 Mock 객체를 사용하는 것을 Mocking이라고 함
-위의 그림은 4-2 데이터 액세스 계층에서 작성한 MemberControllerTest의 postMemberTest() 핸들러 메서드를 실행했을때의 테스트 실행 흐름이다.
-슬라이스 테스트의 목적은 해당 계층 영역에 대한 테스트에 집중하는 것이기 때문에 MemberController만 테스트 하면 되는데 불필요하게 전 계층을 다 거치고 있기 때문에 성능 면에서나 테스트 관심 영역 면에서나 슬라이스 테스트의 주 목적에 맞지 않음
-위의 문제를 해결하기 위해 Mock(가짜) 객체를 사용할 수 있음
-Mock 객체를 사용함으로써 다른 계층과 단절되어 불필요한 과정을 줄이고, 테스트하려는 대상에 집중하여 테스트를 수행할 수 있음
-Mockito
- Spring Framework에서 지원하는 Mocking 라이브러리
- Mock 객체를 생성하고, 해당 Mock 객체가 진짜처럼 동작하게 하는 역할을 담당
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerMockTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private Gson gson;
// (1)
@MockBean
private MemberService memberService;
// (2)
@Autowired
private MemberMapper mapper;
@Test
void postMemberTest() throws Exception {
// given
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
"홍길동",
"010-1234-5678");
Member member = mapper.memberPostToMember(post); // (3)
member.setMemberId(1L); // (4)
// (5)
given(memberService.createMember(Mockito.any(Member.class)))
.willReturn(member);
String content = gson.toJson(post);
// when
ResultActions actions =
mockMvc.perform(
post("/v11/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);
// then
actions
.andExpect(status().isCreated())
.andExpect(header().string("Location", is(startsWith("/v11/members/"))));
}
}
|
cs |
-위의 코드는 Mockito 예시로 MemberController의 postMember() 핸들러 메서드를 테스트하는 케이스임
-----------------------------------------------------------------------------------------------------------------------------------
● postMemberTest() 메서드
(1) @MockBean 애너테이션을 필드에 추가하면 해당 필드의 Bean에 대한 Mock 객체를 생성한 후, 필드에 주입(DI)함
(2) MockMemberService 의 createMember()에서 리턴하는 Member 객체를 생성하기 위해 MemberMapper를 DI 받음
(3) MemberMapper를 이용해 post(MemberDto.Post 타입) 변수를 Member 객체로 변환함
(4) 실제 createMember()의 리턴 값(Member 객체)에는 memberId가 포함이 되는데 이 memberId는 response의 Location header에 포함이 되어야 하므로 (4)와 같이 MockMemberService 의 createMember()에서도 memberId를 리턴해 줄 수 있도록 memberId를 추가함
(5) Mockito에서 지원하는 Stubbing 메서드로
○ given()은 Mock 객체가 특정 값을 리턴하는 동작을 지정하는데 사용하며, Mockito에서 지원하는 when()과 동일한 기능
○ Mockito.any(Member.class) 는 Mockito에서 지원하는 변수 타입 중 하나임
○ .willReturn(member) 는 MockMemberService 의 createMember() 메서드가 리턴 할 Stub 데이터임
-----------------------------------------------------------------------------------------------------------------------------------
+ Stubbing이란 테스트를 위해서 Mock 객체가 항상 일정한 동작을 하도록 지정하는 것을 의미함
+ 위의 코드에서 MockMemberService 클래스는 우리가 테스트하고자 하는 Controller의 테스트에 집중할 수 있도록 다른 계층과의 연동을 끊어주는 역할을 함
+ Spring을 사용하지 않고 JUnit에서 Mockito 기능을 사용하기 위해서는 @ExtendWith(MockitoExtension.class) 애너테이션을 클래스에 추가하고, 필드 객체에 @Mock 애너테이션을 추가하여 Mock 객체로서 생성하고, @InjectMocks 애너테이션을 추가한 필드에 Mock 객체를 주입해줄 수 있음.
-이미지 및 내용 출처 : code states
'부트캠프' 카테고리의 다른 글
Spring MVC - API 문서화 (0) | 2023.03.12 |
---|---|
TDD(Test Driven Development) (0) | 2023.03.12 |
Spring MVC - Testing part 2 (0) | 2023.03.08 |
Spring MVC - Testing part 1 (0) | 2023.03.06 |
Spring MVC - 트랜잭션 (0) | 2023.03.03 |