- 강의
- 104강(SpringBoot와 테스팅) ~ 108강(서블릿 컨테이너와 WEB layer 좀 더 살펴보기)
테스팅
테스트 코드
- 반복되는 검증(테스트) 과정을 줄이는 코드
- 종류
- Acceptance Test(E2E Test)
- 전체 코드 검증
- Integration Test(통합 테스트)
- 전체 코드 검증 → 부분 또는 전체 통합 테스트
- Unit Test
- 코드 일부 테스트
- 종류
- 모킹 유닛 테스트
- service layer 코드를 주로 검증
- 순수 유닛 테스트
- 의부 의존성이 없는 소스 코드 검증
- 외부 라이브러리 동작 검증에 사용
- 모킹 유닛 테스트
- Acceptance Test(E2E Test)
- 사용방법
- 의존성 추가
testImplementation 'org.springframework.boot:spring-boot-starter-test' // test lombok testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok'
- 테스트 코드 구조 = 소스 코드 구조
- resource 폴더 추가 : Mark Directory as → Test Resources Root
- 의존성 추가
테스트 실행 프레임워크
- JUnit5
- @Test : 메서드 실행
- @BeforeEach : 테스트 초기화
- @DisplayName : 테스트 이름 설정
- AssertJ
- JUnit과 함께 사용
- assertEquals() : 기대(예상)와 결과가 동일한지 판단
- assetTure : 참과 거짓 판단
- assertThatExceptionOfType : 예외 검증
- Test Code 시나리오
- Given-When-Then
- Given : 데스트 초기 상태 설정
- When : 테스트 진행
- Then : 기대와 결과가 동일한지 판단
순수 유닛 테스트
- 의부 의존성이 없는 소스 코드 검증
- 외부 라이브러리 동작 검증에 사용
import com.github.supercodingspring.repository.items.ItemEntity;
import com.github.supercodingspring.repository.storeSales.StoreSales;
import com.github.supercodingspring.web.dto.items.Item;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
class ItemMapperUtilTest {
@DisplayName("ItemEntity의 itemEntityToItem 메소드 테스트")
@Test
void ItemEntityToItem() {
// given
ItemEntity itemEntity = ItemEntity.builder()
.name("name")
.type("type")
.id(1)
.price(1000)
.stock(0)
.cpu("CPU 1")
.capacity("5G")
.storeSales(new StoreSales())
.build();
// when
Item item = ItemMapper.INSTANCE.itemEntityToItem(itemEntity);
// then
log.info("만들어진 item: " + item);
assertEquals(itemEntity.getPrice(), item.getPrice());
assertEquals(itemEntity.getId().toString(), item.getId());
assertEquals(itemEntity.getCapacity(), item.getSpec().getCapacity());
assertEquals(itemEntity.getCpu(), item.getSpec().getCpu());
}
}
모킹 유닛 테스트
- service layer 코드를 주로 검증
- Mocking
- 테스트 진행 시 의존성 문제를 해결하기 위해서 가짜 객체를 생성해서 사용하는 방법
- 라이브러리 : mockito
- mock(가짜 객체) 생성 및 주입
- when으로 행동 정의
- 생성자 주입이 안되기 때문에 필드 주입 사용하기
import com.github.supercodingspring.repository.airlineTicket.*;
import com.github.supercodingspring.repository.users.*;
import com.github.supercodingspring.service.exceptions.*;
import com.github.supercodingspring.web.dto.airline.Ticket;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@Slf4j
class AirReservationServiceUnitTest {
@Mock // mock 객체
private UserJpaRepository userJpaRepository;
@Mock // mock 객체
private AirlineTicketJpaRepository airlineTicketJpaRepository;
@InjectMocks // 생성한 mock 객체 주입
private AirReservationService airReservationService;
@BeforeEach
public void setUp(){
MockitoAnnotations.openMocks(this);
}
@DisplayName("airlineTicket에 해당하는 유저 항공권들이 모두 있어서 성공하는 경우")
@Test
void FindUserFavoritePlaceTicketsCase1() {
// given
Integer userId = 5;
String likePlace = "파리";
String ticketType = "왕복";
UserEntity userEntity = UserEntity.builder()
.userId(userId)
.likeTravelPlace(likePlace)
.userName("name1")
.phoneNum("1234")
.build();
List<AirlineTicket> airlineTickets = Arrays.asList(
AirlineTicket.builder()
.ticketId(1)
.arrivalLocation(likePlace)
.ticketType(ticketType)
.build(),
AirlineTicket.builder()
.ticketId(2)
.arrivalLocation(likePlace)
.ticketType(ticketType)
.build(),
AirlineTicket.builder()
.ticketId(3)
.arrivalLocation(likePlace)
.ticketType(ticketType)
.build(),
AirlineTicket.builder()
.ticketId(4)
.arrivalLocation(likePlace)
.ticketType(ticketType)
.build()
);
// when
when(userJpaRepository.findById(any())).thenReturn(Optional.of(userEntity));
when(airlineTicketJpaRepository.findAirlineTicketsByArrivalLocationAndTicketType(likePlace, ticketType))
.thenReturn(airlineTickets);
// then
List<Ticket> tickets = airReservationService.findUserFavoritePlaceTickets(userId, ticketType);
log.info("tickets: " + tickets);
assertTrue(
tickets.stream()
.map(Ticket::getArrival)
.allMatch((arrival) -> arrival.equals(likePlace))
);
}
@DisplayName("User를 찾을 수 없는 경우, Exception 발생해야 함 ")
@Test
void FindUserFavoritePlaceTicketsCase4() {
// given
Integer userId = 5;
String likePlace = "파리";
String ticketType = "왕복";
UserEntity userEntity = null;
List<AirlineTicket> airlineTickets = Arrays.asList(
AirlineTicket.builder().ticketId(1).arrivalLocation(likePlace).ticketType(ticketType).build(),
AirlineTicket.builder().ticketId(2).arrivalLocation(likePlace).ticketType(ticketType).build(),
AirlineTicket.builder().ticketId(3).arrivalLocation(likePlace).ticketType(ticketType).build(),
AirlineTicket.builder().ticketId(4).arrivalLocation(likePlace).ticketType(ticketType).build()
);
// when
when(userJpaRepository.findById(any())).thenReturn(Optional.ofNullable(userEntity));
when(airlineTicketJpaRepository.findAirlineTicketsByArrivalLocationAndTicketType(likePlace, ticketType))
.thenReturn(airlineTickets);
// then
assertThrows(NotFoundException.class,
() -> airReservationService.findUserFavoritePlaceTickets(userId, ticketType)
);
}
}
통합 슬라이스 테스트
- 원하는 layer만 테스트하는 방식 → 실제 생성자 호출
- 실제 해당하는 layer 속하는 bean만 호출 → 빠르게 테스트 가능
- ex) Repository Layer 테스트를 한다면 @Controller, @Service 빈이 아닌 @Repository 빈만 호출
- @WebMvcTest : Web Layer(Controller 부분) 슬라이스 테스트
- @DataJpaTest : Repository layer 슬라이스 테스트
- 실제 DB와 연결함
import com.github.supercodingspring.repository.airlineTicket.*;
import com.github.supercodingspring.repository.passenger.*;
import com.github.supercodingspring.service.AirReservationService;
import lombok.extern.slf4j.Slf4j;
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.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest // slice test => Dao Lay / Jpa 사용하고 있는 Slice Test
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Slf4j
class ReservationJpaRepositoryJpaTest {
// 다른 layer의 생성자 주입하면 안됨
// @Autowired
// private AirReservationService airReservationService;
// 생성자 주입
@Autowired
private ReservationJpaRepository reservationJpaRepository;
@Autowired
private PassengerJpaRepository passengerJpaRepository;
@Autowired
private AirlineTicketJpaRepository airlineTicketJpaRepository;
@DisplayName("ReservationRepository로 항공편 가격과 수수료 검색")
@Test
void FindFlightPriceAndCharge() {
// given
Integer userId = 10;
// when
List<FlightPriceAndCharge> flightPriceAndCharges = reservationJpaRepository.findFlightPriceAndCharge(userId);
// then
log.info("결과: " + flightPriceAndCharges);
}
@DisplayName("Reservation 예약 진행")
@Test
void saveReservation(){
// given
Integer userId = 10;
Integer ticketId = 5;
Passenger passenger = passengerJpaRepository.findPassengerByUserUserId(userId).get();
AirlineTicket airlineTicket = airlineTicketJpaRepository.findById(5).get();
// when
Reservation reservation = new Reservation(passenger, airlineTicket);
Reservation res = reservationJpaRepository.save(reservation);
// then
log.info("결과: " + res);
assertEquals(res.getPassenger(), passenger);
assertEquals(res.getAirlineTicket(), airlineTicket);
}
}
전체 통합 테스트
- Mocking Bean 없이 모든 Bean을 호출하여 전체 코드 테스트하는 방법
- MockMVC : 실제 API 호출하듯이 해서 Mock이 붙여짐, Mock을 사용해서 그렇게 하는 것은 아님
- @SpringBootTest : 통합 테스트
import lombok.extern.slf4j.Slf4j;
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.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Slf4j
class AirReservationControllerSpringTest {
@Autowired
private MockMvc mockMvc;
@DisplayName("Find Airline Tickets 성공")
@Test
void FindAirlineTickets() throws Exception {
// given
Integer userId = 5;
String ticketType = "왕복";
// when & then
String content = mockMvc.perform(
get("/v1/api/air-reservation/tickets")
.param("user-Id", userId.toString())
.param("airline-ticket-type", ticketType)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
log.info("결과: " + content);
}
@DisplayName("Find Airline Tickets 실패 1")
@Test
void FindAirlineTicketsCase2() throws Exception {
// given
Integer userId = 5;
String ticketType = "왕";
// when & then
String content = mockMvc.perform(
get("/v1/api/air-reservation/tickets")
.param("user-Id", userId.toString())
.param("airline-ticket-type", ticketType)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().is4xxClientError())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
log.info("결과: " + content);
}
}
실제 서버 업무
- 비즈니스 로직 구현
- 로그인 이슈, 보안 이슈 → 쿠키 & 섹션 & 토큰, Spring Security
- 성능 및 속도 이슈, 트래픽 쏠림 이슈 → 서블릿 컨테이너, Web layer, HTTp 캐싱과 SpringBoot 캐싱
서블릿 컨테이너
- WAS의 HTTP 요청시 → Servlet Requet/Response 생성
- HttpServletRequest
- 요청 정보를 서블릿에게 전달하기 위한 객체
- header, URL, method 등을 확인하는 메서드 존재
- Body Stream을 읽는 메서드 존재
- HttpServletResponse
- 요청을 보낸 클라이언트의 응답을 보내는 객체
- 응답 정보 전송
- HTTP Request/Response ↔ HttpServletRequest/HttpServletResponse ↔ Spring Container
- HttpServletRequest
- Filter와 Interceptor가 추가된 요청 과정
- 클라이언트 요청 → Filter Chain → Servlet Container → HTTP Servlet → Filters → Spring Container → Dispatcher Servlet → Interceptor → AOP(Advice) → @RestController, @Controller
Filter
- = Web Filter
- 특징
- Spring Container 밖에 있고, Spring 자원 X
- doFilter() 메서드로 request/response 동작
- HttPServlet의 Request와 Response 객체에 관여
- 기능
- 이미지 및 데이터 압축 및 문자열 인코딩
- 모든 요청에 대한 로깅
- 공통 보안 및 인증/인가
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@Slf4j
public class LoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String method = request.getMethod();
String uri = request.getRequestURI();
log.info(method + uri + " 요청이 들어왔습니다.");
filterChain.doFilter(request, response);
log.info(method + uri + "가 상태 " + response.getStatus() + " 로 응답이 나갑니다." );
}
}
Interceptor
- 특징
- Spring Container 자원
- preHandler(), postHandler()로 요청과 응답 분리
- HttpServlet의 Request와 Response 객체에 관여 X → 주로 요청 전후에 특정 작업을 수행하기 위해 사용되지만, 모든 시점에서 이 객체들에 대해 원하는 모든 작업을 할 수 있는 것 X
- 주사용 기능
- API 호출 시간 로깅
- Session과 Cookie 확인
- 세부적인 보안 및 인증
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class RequestTimeLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
long endTime = System.currentTimeMillis();
long executeTIme = endTime - startTime;
log.info("{} {} executed in {} ms", request.getMethod(), request.getRequestURI(), executeTIme);
}
}
import com.github.supercodingspring.web.Interceptors.RequestTimeLoggingInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final RequestTimeLoggingInterceptor requestTimeLoggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestTimeLoggingInterceptor);
}
}
'슈퍼코딩 > 주특기(JAVA)' 카테고리의 다른 글
2024.06.07(금) 슈퍼코딩 신입연수원 10주차 Day 5 후기 - Spring Security, 보안기초 (0) | 2024.06.07 |
---|---|
2024.06.06(목) 슈퍼코딩 신입연수원 10주차 Day 4 후기 - Cache, Cookie & Session & JWT (1) | 2024.06.06 |
2024.06.04(화) 슈퍼코딩 신입연수원 10주차 Day 2 후기 - JPA, PSA (0) | 2024.06.04 |
2024.06.03(월) 슈퍼코딩 신입연수원 10주차 Day 1 후기 - 예외 처리, AOP (0) | 2024.06.03 |
슈퍼코딩 신입연수원 9주차 후기 (0) | 2024.06.01 |