[애플리케이션 테스트 종류]
1. 기능 테스트
애플리케이션을 사용하는 사용자 입장에서 애플리케이션이 제공하는 기능이 제대로 동작하는지 테스트.
API 툴이나 DB까지 연관되어 있어서 HTTP 통신도 해야되고, DB 연결도 해야되는 등
우리가 개발한 애플리케이션과 연관된 대상이 많기 때문에 단위 테스트라고 부르기엔 힘들다.
2. 통합 테스트
클라이언트 측 툴 없이 개발자가 짜 놓은 테스트 코드를 실행시켜서 이루어지는 경우.
Controller의 API를 호출하는 테스트 코드를 작성한 후
실행하면 Service 계층과 DB 계층을 거쳐 DB에 실제로 접속해서
기대했던 대로 동작하는지 테스트 하는 것.
여러 계층이 연관되어 있으므로 여전히 단위 테스트라고 하기엔 어렵다.
3. 슬라이스 테스트
애플리케이션을 특정 계층으로 쪼개어서 하는 테스트.
4. 단위 테스트 : 메서드 단위로 테스트 하는 코드.
[Given - When - Then 구조]
Given
테스트를 위한 준비 과정을 명시한다.
테스트에 필요한 전제 조건들이 포함된다.
테스트 대상에 전달되는 입력 값(데스트 데이터)
When
테스트 할 동작(대상)을 지정.
단위 테스트에서 일반적으로 메서드 호출을 통해 테스트를 진행하므로 한 두줄 정도로 끝남.
Then
테스트의 결과를 검증하는 영역.
일반적으로 예상하는 값(expected)와 테스트 대상 메서드의 동작 수행 결과(Actual) 값을 비교해서
기대한대로 동작을 수행하는지 검증(Assertion)하는 코드들이 포함.
[Assertion]
예상하는 테스트의 결과가 True이길 바라는 것.
[Assertion 메서드 사용하기]
1. assertEquls(A, B) : A와 B가 같으면 pass, 다르면 not pass
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HelloJUnitTest {
@DisplayName("Hello JUnit Test") // (1)
@Test
public void assertionTest() {
String expected = "Hello, JUnit";
String actual = "Hello, JUnit";
assertEquals(expected, actual); // (2)
}
}
2. assertNotNull(A, B) : A에 해당하는 값이 null이 아니면 pass, null이면 B가 출력됨
import com.codestates.CryptoCurrency;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class AssertionNotNullTest {
@DisplayName("AssertionNull() Test")
@Test
public void assertNotNullTest() {
String currencyName = getCryptoCurrency("ETH");
// (1)
assertNotNull(currencyName, "should be not null");
}
private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit);
}
}
import java.util.HashMap;
import java.util.Map;
public class CryptoCurrency {
public static Map<String, String> map = new HashMap<>();
static {
map.put("BTC", "Bitcoin");
map.put("ETH", "Ethereum");
map.put("ADA", "ADA");
map.put("POT", "Polkadot");
}
}
3. assertThrows(A.class, B) : A.class => 예상되는 예외 클래스, B : 테스트 대상 메서드
public class AssertionExceptionTest {
@DisplayName("throws NullPointerException when map.get()")
@Test
public void assertionThrowExceptionTest() {
assertThrows((1)NullPointerException.class, (2)() -> getCryptoCurrency("XRP"));
}
private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit).toUpperCase();
}
}
예외 클래스간의 상관 관계
1위 Exception.class > 2위 RuntimeException.class > 3위 NullPointerException.class
4. assertDoesNotThrow( () -> 호출하고자 하는 메서드)
예외가 발생하지 않을 것으로 예상되는 메서드. 예외 미발생 passed, 예외 발생 no pass.
[테스트 케이스 실행 전, 전처리]
1. @BeforeEach 애너테이션이 추가된 메서드는
각각의 테스트 케이스가 실행될 때 마다 각각의 테스트 케이스 실행 직전에 먼저 실행되어 초기화 가능.
[@BeforeEach 예시1]
public class BeforeEach1Test {
@BeforeEach
public void init() {
System.out.println("Pre-processing before each test case");
}
@DisplayName("@BeforeEach Test1")
@Test
public void beforeEachTest() {
}
@DisplayName("@BeforeEach Test2")
@Test
public void beforeEachTest2() {
}
}
[@BeforeEach 예시2]
public class BeforeEach2Test {
private Map<String, String> map;
@BeforeEach
public void init() {
map = new HashMap<>();
map.put("BTC", "Bitcoin");
map.put("ETH", "Ethereum");
map.put("ADA", "ADA");
map.put("POT", "Polkadot");
}
@DisplayName("Test case 1")
@Test
public void beforeEachTest() {
map.put("XRP", "Ripple");
assertDoesNotThrow(() -> getCryptoCurrency("XRP"));
}
@DisplayName("Test case 2")
@Test
public void beforeEachTest2() {
System.out.println(map);
assertDoesNotThrow(() -> getCryptoCurrency("XRP"));
}
private String getCryptoCurrency(String unit) {
return map.get(unit).toUpperCase();
}
}
2. @BeforeAll 애너테이션은 static 메서드에만 사용되며, 이는
테스트 케이스를 한번에 실행 시키면 테스트 케이스가 실행되기 직전에 딱 한번만 초기화 작업을 진행.
[@BeforeAll 예시]
public class BeforeAllTest {
private static Map<String, String> map;
@BeforeAll
public static void initAll() {
map = new HashMap<>();
map.put("BTC", "Bitcoin");
map.put("ETH", "Ethereum");
map.put("ADA", "ADA");
map.put("POT", "Polkadot");
map.put("XRP", "Ripple");
System.out.println("initialize Crypto Currency map");
}
@DisplayName("Test case 1")
@Test
public void beforeEachTest() {
assertDoesNotThrow(() -> getCryptoCurrency("XRP"));
}
@DisplayName("Test case 2")
@Test
public void beforeEachTest2() {
assertDoesNotThrow(() -> getCryptoCurrency("ADA"));
}
private String getCryptoCurrency(String unit) {
return map.get(unit).toUpperCase();
}
}
3. @AfterEach, @AfterAll은 호출되는 시점이 반대일 뿐 동작 방식은 동일하다.
[슬라이스 테스트]
각 계층에 구현해놓은 기능들이 잘 동작하는지 특정 계층만 잘라서 하는 테스트.
1. API 계층 테스트 - Controller 테스트.
@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 검증
}
}
(1) @SpringBootTest
Spring Boot 기반의 애플리케이션을 테스트하기 위한 Application Context를 생성.
(2) @AutoConfigureMockMvc
Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 진행.
(3) @Autowired
DI로 주입 받은 MockMvc는 서버를 실행하지 않고, 애플리케이션의 Controller를 테스트할 환경을 지원.
=> MockMvc 같은 기능을 사용하려면 반드시 @AutoConfigureMockMvc 애너테이션을 추가해야한다.
(4) @Test
테스트하고자 하는 Controller 핸들러 메서드의 테스트 케이스를 작성.
(5) Controller를 테스트하기 위해서 Request body를 만드는 곳. Given
(6)
MockMvc객체를 통해 요청 URI와 HTTP 메서드를 지정하고,
(5)에서 만든 Request body를 추가한 뒤 request 수행. When
(7)
Controller에서 전달 받은 HTTP Status와 response body 데이터를 검증 작업 진행. Then
2. 데이터 액세스 계층 테스트
import com.codestates.member.entity.Member;
import com.codestates.member.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.junit.jupiter.api.Assertions.*;
@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()));
}
}
(1) @DataJpaTest : 데이터 액세스 계층 테스트의 핵심 애너테이션.
MemberRepository의 기능을 정상적으로 사용하기 위한 Configuration을 Spring이 자동으로 해줌.
또한 @Transactional 애너테이션을 포함하고 있기 때문에 테스트 케이스 실행이 종료되는 시점에
DB에 저장된 데이터는 rollback 처리로 테스트 이후 DB 상태가 초기화 된다.
(2) MemberRepository를 DI 받는다.
GIven
(3) 테스트할 회원 정보 데이터 생성
When
(4) 회원 정보 저장
Then
(5) 저장이 잘 되었는지 검증.
(5-1) 저장된 객체가 null이 아닌지 검증.
리턴으로 반환된 Member 객체(savedMember)의 필드가 (3) 테스트 데이터와 일치하는지 검증.
import com.codestates.member.entity.Member;
import com.codestates.member.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
public class MemberRepositoryTest {
...
...
@Test
public void findByEmailTest() {
// given (1)
Member member = new Member();
member.setEmail("hgd@gmail.com");
member.setName("홍길동");
member.setPhone("010-1111-2222");
// when
memberRepository.save(member); // (2)
Optional<Member> findMember = memberRepository.findByEmail(member.getEmail()); // (3)
// then (4)
assertTrue(findMember.isPresent()); // (4-1)
assertTrue(findMember.get().getEmail().equals(member.getEmail())); // (4-2)
}
}
Given
(1) 테스트할 회원 객체를 생성.
When
(2) 회원 정보 저장
(3) 저장 후 리턴되는 Member객체를 이용하는 것이 아닌, (2)에서 저장한 정보 중
email에 해당하는 정보를 잘 조회하는지 테스트하기 위해 findByEmail()으로 회원 정보를 조회.
Then
(4) 조회한 정보가 null이 아닌지 검증
(4) findByEmail() 메서드로 조회한 정보가 테스트 객체의 email과 동일한지 검증.
[Mockito]
테스트하고자 하는 대상에서 다른 영역(다른 계층)을 단절시켜 오로지 테스트 대상에만 집중하게 해줌.
[Controller 클래스 필드]
@Transactional
@SpringBootTest
@AutoConfigureMockMvc
public class MemberControllerHomeworkTest {
@Autowired
(1) private MockMvc mockMvc;
@Autowired
(2) private Gson gson;
@MockBean
(3) private MemberService memberService;
@Autowired
(4) private MemberMapper mapper;
(1) 프레임워크 MockMvc 주입.
(2) 요청을 JSON으로 변경하는 라이브러리 GSON 주입.
(3) MemberService를 @MockBean으로 설정해 Spring Context에 넣고 Mock객체로 선언.
(4) API 계층과 Service 계층간에 데이터 타입을 변환해줄 Mapper를 주입.
[postMember()]
@Test
void postMemberTest() throws Exception {
(1) MemberDto.Post post = new MemberDto.Post("ah@ah.com","아이언맨","010-1111-1111");
(2) Member member = mapper.memberPostToMember(post);
(3) member.setMemberId(1L);
(4) given(memberService.createMember(Mockito.any(Member.class)))
(5) .willReturn(member);
// mock객체 memberService의 메서드 createMember 실행하고 리턴 member stubbing.
(6) String content = gson.toJson(post);
(7) ResultActions actions =
(8) mockMvc.perform(
(9) post("/v11/members")
(10) .accept(MediaType.APPLICATION_JSON)
(11) .contentType(MediaType.APPLICATION_JSON)
(12) .content(content)
);
(13) actions.andExpect(status().isCreated())
(14) .andExpect(header().string("Location", is(startsWith("/v11/members/"))));
}
}
(1) : DTO.Post 객체로 요청 테스트 멤버 생성.
(2) : mapper 객체를 활용해 Service 계층 메서드를 이용하기 위해 Member 타입으로 변환.
(3) : 테스트 member 객체에 Id값을 할당.
(4) : 실제 MemberService의 createMember() 메서드를 mock-memberService로 호출하고,
실제 createMember()가 받는 타입이 Member이므로 Member.class를 주입.
(5) : 실제 createMember()가 받는 객체인 member를 넣어주는데 이 객체는 임의 정보이므로 stub 객체.
(6) : 테스트로 만든 post 객체를 JSON으로 변환.
(7) : 요청에 대한 응답 데이터 타입인 ResultActions 생성.
(8) : mockMvc.perform() : 요청을 전송하겠다 ~ 라는 의미.
(9) : 요청을 전달할 HTTP Method와 도착할 URL 설정.
(10) : accept 서버가() 클라이언트의 요청에 대한 응답 데이터 타입을 JSON으로 하겠다 ~ 라는 뜻.
(11) : contentType() 클라이언트가 서버에게 전달할 요청 데이터 타입을 JSON으로 주겠다 ~ 라는 뜻.
(12) : content() 요청 데이터 : 요청할 데이터는 POST 테스트 객체를 GSON 객체로 JSON으로 변환한 것.
(13) : 요청에 대한 응답 데이터에 포함될 예상되는 Http Status.
(14) : 요청에 대한 응답 데이터에 포함되는 header의 Location이 URL 이라고 예상.
[patchMember()]
@Test
void patchMemberTest() throws Exception {
(1) MemberDto.Patch patch = new MemberDto.Patch();
(2) Member member = mapper.memberPatchToMember(patch);
(3) member.setName("한다현");
member.setMemberStatus(Member.MemberStatus.MEMBER_SLEEP);
member.setStamp(new Stamp());
member.setPhone("010-3333-3333");
member.setEmail("han@han.com");
(4) given(memberService.updateMember(Mockito.any(Member.class)))
(5) .willReturn(member);
(6) String content = gson.toJson(patch);
(7) ResultActions patchActions =
(8) mockMvc.perform(
(9) patch("/v11/members/1")
(10) .contentType(MediaType.APPLICATION_JSON)
(11) .accept(MediaType.APPLICATION_JSON)
(12) .content(content)
);
(13) patchActions.andExpect(status().isOk())
(14) .andExpect(jsonPath("$.data.name").value(member.getName()))
(15) .andExpect(jsonPath("$.data.email").value(member.getEmail()))
(16) .andExpect(jsonPath("$.data.phone").value(member.getPhone()))
(17) .andExpect(jsonPath("$.data.memberId").value(0));
}
(1) : 비어있는 PatchDto 객체를 생성.
(2) : mapper를 이용하여 빈 PatchDto객체를 Member타입으로 변환.
(3) : member객체에 내용 할당.
(4) : 실제 MemberService의 updateMember() 메서드를 mock-memberService로 호출하고,
실제 updateMember()가 받는 타입이 Member이므로 Member.class를 주입.
(5) : willReturn() 실제 updateMember()가 받는 객체인 member를 넣어준다. (2)에서 만든 stub data.
(6) : 테스트로 만든 patch 객체를 JSON으로 변환.
(7) : 응답 데이터 타입인 ResultActions 생성.
(8) : mockMvc.perform() : 요청을 전송하겠다 ~ 라는 의미.
(9) : 요청을 전달할 HTTP Method와 도착할 URL 설정.
(10) : contentType() 클라이언트가 서버에게 전달할 요청 데이터 타입을 JSON으로 주겠다 ~ 라는 뜻.
(11) : accept 서버가() 클라이언트의 요청에 대한 응답 데이터 타입을 JSON으로 하겠다 ~ 라는 뜻.
(12) : content() 요청 데이터 : 요청할 데이터는 patch 테스트 객체를 GSON 객체로 JSON으로 변환한 것.
(13) : 응답 데이터에 포함될 예상되는 Http Status.
(14) ~ (17) : 응답 데이터에 포함되리라 예상하는 필드값들이 member객체의 필드와 같다고 예상.
[getMember()]
@Test
void getMemberTest() throws Exception {
(1) MemberDto.Post post = new MemberDto.Post("han@gmai.com","홍길동","010-1111-1111");
(2) Member member = mapper.memberPostToMember(post);
(3) member.setStamp(new Stamp());
(4) long memberId = 1L;
(5) given(memberService.findMember(Mockito.any(Long.class)))
(6) .willReturn(member);
(7) ResultActions getMemberActions = mockMvc.perform(
(8) get("/v11/members/{member-id}",memberId)
(9) .accept(MediaType.APPLICATION_JSON)
);
(10) getMemberActions.andExpect(status().isOk())
.andExpect(jsonPath("$.data.email").value(post.getEmail()))
.andExpect(jsonPath("$.data.name").value(post.getName()))
.andExpect(jsonPath("$.data.phone").value(post.getPhone()));
}
(1) : Dto.Post 객체 post를 정보를 넣어 생성한다.
(2) : mapper를 이용하여 PostDto객체를 Member타입으로 변환.
(3) : member객체에 추가 데이터 할당.
(4) : memberId 값을 초기화해줌.
(5) : getMember은 DB에 저장된 객체를 확인하는 메서드이므로 mock-memberService 객체의
findMember()메서드를 사용하며 파라미터로는 식별자 id는 long 타입이므로 Long.class를 넣어줌.
(6) : findMember()메서드의 반환 타입인 member를 넣어줌.
(7) : 응답 데이터 타입인 ResultActions 생성. mockMvc.perform() : 요청을 전송하겠다 ~ 라는 의미.
(8) : 요청을 전달할 HTTP Method와 도착할 URL, URL에 나와있는 memberId 전달.
(9) : accept 서버가() 클라이언트의 요청에 대한 응답 데이터 타입을 JSON으로 하겠다 ~ 라는 뜻.
(10) : 응답데이터의 status는 isOk()임과 email, name, phone이 post에서 생성한 정보와 같다고 예상.
[getMembers()]
@Test
void getMembersTest() throws Exception {
(1) Member member1 = new Member("han@gmail.com","아이언맨","010-1234-1234");
member1.setStamp(new Stamp());
Member member2 = new Member("han11@gmail.com","캡틴아메리카","010-1234-4321");
member2.setStamp(new Stamp());
(2) int page = 1;
int size = 10;
(3) List<Member> memberList = List.of(member1,member2);
(4) Page<Member> pageMembers = new PageImpl<>(memberList, PageRequest.of(page -1, size,
Sort.by("memberId").descending()),memberList.size());
(5) given(memberService.findMembers(Mockito.any(Integer.class),Mockito.any(Integer.class)))
// Q3. willReturn() 안에는 findMembers 메서드의 리턴값이 들어가야하는데
pageMembers는 findMembers 메서드 리턴값이 아니지않나요?
(6) .willReturn(pageMembers);
(7) ResultActions membersActions = mockMvc.perform(
(8) get("/v11/members")
(9) .param("page",String.valueOf(page))
(10) .param("size",String.valueOf(size))
(11) .accept(MediaType.APPLICATION_JSON)
);
(12) membersActions.andExpect(status().isOk())
.andExpect(jsonPath("$.data[0].email").value("han@gmail.com"))
.andExpect(jsonPath("$.data[1].email").value("han11@gmail.com"))
.andExpect(jsonPath("$.data[0].name").value("아이언맨"))
.andExpect(jsonPath("$.data[1].name").value("캡틴아메리카"));
} }
(1) : 테스트할 member 객체 2개 생성.
(2) : 정보들을 나열할 page와 size를 지정.
(3) : 나열할 객체를 List 형태로 구성.
(4) : 페이지네이션 사용.
(5) : mock-memberService 객체를 사용해 findMembers() 메소드의 파라미터는 page, size 이므로
Integer.class를 입력해준다.
(6) : findMembers()의 반환 타입인 pageMembers를 넣어준다.
(7) : 응답 데이터 타입인 ResultActions 생성. mockMvc.perform() : 요청을 전송하겠다 ~ 라는 의미.
(8) : 요청을 전달할 HTTP Method와 도착할 URL 설정.
(9), (10) : page, size에 해당하는 내용.
(11) : accept () 서버가 클라이언트의 요청에 대한 응답 데이터 타입을 JSON으로 하겠다 ~ 라는 뜻.
(12) : List은 인덱스로 정보를 관리하므로 0번, 1번 인덱스를 표기하여 해당하는 필드.value()로 호출.
[deleteMembers()]
@Test
void deleteMemberTest() throws Exception {
(1) doNothing().when(memberService).deleteMember(Mockito.anyLong());
(2) mockMvc.perform(
(3) delete("/v11/members/1")
(4) .accept(MediaType.APPLICATION_JSON)
);
}
(1) : doNothing().when(동작할 객체).해당 객체의 메서드(메서드의 파라미터 타입)
doNothing().when(memberService).deleteMember(Mockito.anyLong());
(2) : 요청을 전송하겠다 ~
(3) : Http Method와 URL
(4) : 서버는 클라이언트에게 받을 요청 데이터의 타입을 지정.
'백엔드 학습 과정 > Section 3 [Spring MVC, JDBC, JPA, RestDo' 카테고리의 다른 글
#10. 애플리케이션 빌드 (0) | 2023.01.14 |
---|---|
#9. API 문서화 (0) | 2023.01.14 |
#7. Spring MVC 트랜잭션 (0) | 2023.01.11 |
#6. Spring Data JPA (0) | 2023.01.11 |
#5. JPA (0) | 2023.01.01 |