💚 Spring
통합 테스트와 Spring AOP(어드바이스, 포인트컷)
MNY
2024. 6. 14. 01:18
728x90
반응형
2024.06.13.(목)
💡 오늘의 학습 키워드
- Spring 심화주차 1주차 -
mockito
단위 테스트 vs 통합 테스트
Security Test
MockBean
Spring AOP(어드바이스, 포인트컷)
mockito
: 단위 테스트를 위해 모의 객체를 생성하고 관리하는 데 사용되는 Java 오픈소스 프레임워크
- 실제 객체의 동작을 모방하는 모의 객체(Mock Object)를 생성하여 코드의 '특정 부분을 격리'시키고 테스트하기 쉽게 만들어 준다.
- 주로 단일 컴포넌트의 동작을 테스트하는 데 사용되며 클래스 내의 개별 메서드나 함수, 서로 다른 클래스 또는 컴포넌트 간의 상호작용, 객체들 간의 협업 등을 테스트할 수 있다.
@Mock
ProductRepository productRepository;
@Mock
FolderRepository folderRepository;
@Mock
ProductFolderRepository productFolderRepository;
@Test
@DisplayName("관심 상품 희망가 - 최저가 이상으로 변경")
void test1() {
// given
Long productId = 100L;
int myprice = ProductService.MIN_MY_PRICE + 3_000_000;
ProductMypriceRequestDto requestMyPriceDto = new ProductMypriceRequestDto();
requestMyPriceDto.setMyprice(myprice);
User user = new User();
ProductRequestDto requestProductDto = new ProductRequestDto(
"Apple <b>맥북</b> <b>프로</b> 16형 2021년 <b>M1</b> Max 10코어 실버 (MK1H3KH/A) ",
"https://shopping-phinf.pstatic.net/main_2941337/29413376619.20220705152340.jpg",
"https://search.shopping.naver.com/gate.nhn?id=29413376619",
3515000
);
Product product = new Product(requestProductDto, user);
ProductService productService = new ProductService(productRepository, folderRepository, productFolderRepository);
given(productRepository.findById(productId)).willReturn(Optional.of(product));
// when
ProductResponseDto result = productService.updateProduct(productId, requestMyPriceDto);
// then
assertEquals(myprice, result.getMyprice());
}
단위 테스트(Unit Test) vs 통합 테스트(Integration Test)
- 단위 테스트(Unit Test)
- 하나의 모듈이나 클래스에 대해 세밀한 부분까지 테스트 가능
- 하지만 모듈 간에 상호 작용 검증 X
- 단위 테스트 시 Spring은 동작되지 X
- 통합 테스트(Integration Test)
- 두 개 이상의 모듈이 연결된 상태 테스트 가능
- 모듈 간의 연결에서 발생하는 에러 검증 가능
- 여러 단위 테스트를 하나의 통합된 테스트로 수행
- @SpringBootTest : 테스트 수행 시 스프링이 동작
** 통합 테스트를 위해 해당 어노테이션 사용
Security Test
- SecurityContextHolder
- 인증 객체(Context)를 담는 공간
- SecurityContext에 접근하기 위해서 필요
- SecurityContextHolder.getContext() 하면 인증 객체(Context)가 반환
- setAuthentication : 인증 객체를 만들어 주는 것
- 예시 코드(더보기 Click)
더보기
- test > mvc > MockSpringSecurityFilter
package com.sparta.myselectshop.mvc;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.io.IOException;
public class MockSpringSecurityFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
SecurityContextHolder.getContext()
.setAuthentication((Authentication) ((HttpServletRequest) req).getUserPrincipal());
chain.doFilter(req, res);
}
@Override
public void destroy() {
SecurityContextHolder.clearContext();
}
}
- test > mvc > UserProductMvcTest
package com.sparta.myselectshop.mvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.myselectshop.config.WebSecurityConfig;
import com.sparta.myselectshop.controller.ProductController;
import com.sparta.myselectshop.controller.UserController;
import com.sparta.myselectshop.dto.ProductRequestDto;
import com.sparta.myselectshop.entity.User;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.security.UserDetailsImpl;
import com.sparta.myselectshop.service.FolderService;
import com.sparta.myselectshop.service.KakaoService;
import com.sparta.myselectshop.service.ProductService;
import com.sparta.myselectshop.service.UserService;
import org.junit.jupiter.api.BeforeEach;
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.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;
import java.security.Principal;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@WebMvcTest(
controllers = {UserController.class, ProductController.class},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = WebSecurityConfig.class
)
}
)
class UserProductMvcTest {
private MockMvc mvc;
private Principal mockPrincipal;
@Autowired
private WebApplicationContext context;
@Autowired
private ObjectMapper objectMapper;
@MockBean
UserService userService;
@MockBean
KakaoService kakaoService;
@MockBean
ProductService productService;
@MockBean
FolderService folderService;
@BeforeEach
public void setup() {
mvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity(new MockSpringSecurityFilter()))
.build();
}
private void mockUserSetup() {
// Mock 테스트 유져 생성
String username = "sollertia4351";
String password = "robbie1234";
String email = "sollertia@sparta.com";
UserRoleEnum role = UserRoleEnum.USER;
User testUser = new User(username, password, email, role);
UserDetailsImpl testUserDetails = new UserDetailsImpl(testUser);
mockPrincipal = new UsernamePasswordAuthenticationToken(testUserDetails, "", testUserDetails.getAuthorities());
}
@Test
@DisplayName("로그인 Page")
void test1() throws Exception {
// when - then
mvc.perform(get("/api/user/login-page"))
.andExpect(status().isOk())
.andExpect(view().name("login"))
.andDo(print());
}
@Test
@DisplayName("회원 가입 요청 처리")
void test2() throws Exception {
// given
MultiValueMap<String, String> signupRequestForm = new LinkedMultiValueMap<>();
signupRequestForm.add("username", "sollertia4351");
signupRequestForm.add("password", "robbie1234");
signupRequestForm.add("email", "sollertia@sparta.com");
signupRequestForm.add("admin", "false");
// when - then
mvc.perform(post("/api/user/signup")
.params(signupRequestForm)
)
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/api/user/login-page"))
.andDo(print());
}
@Test
@DisplayName("신규 관심상품 등록")
void test3() throws Exception {
// given
this.mockUserSetup();
String title = "Apple <b>아이폰</b> 14 프로 256GB [자급제]";
String imageUrl = "https://shopping-phinf.pstatic.net/main_3456175/34561756621.20220929142551.jpg";
String linkUrl = "https://search.shopping.naver.com/gate.nhn?id=34561756621";
int lPrice = 959000;
ProductRequestDto requestDto = new ProductRequestDto(
title,
imageUrl,
linkUrl,
lPrice
);
String postInfo = objectMapper.writeValueAsString(requestDto);
// when - then
mvc.perform(post("/api/products")
.content(postInfo)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.principal(mockPrincipal)
)
.andExpect(status().isOk())
.andDo(print());
}
}
- config > JpaConfig
@Configuration // 아래 설정을 등록하여 활성화 합니다.
@EnableJpaAuditing // 시간 자동 변경이 가능하도록 합니다.
public class JpaConfig {
}
MockBean
: Controller 사용하려면 Service의 Bean이 필요한데 각자의 빈을 생성해주는 것
Spring AOP(어드바이스, 포인트컷)
- 어드바이스 : 부가기능
- 우리가 만든 부가기능을 핵심 기능 언제 수행할 건지를 정하는 것
- 그 언제가 핵심 기능 전일 수도 후일 수도 있고 전, 후 전부일 수도 있고 핵심 기능이 오류가 발생했을 때 일 수도 있다. 혹은 반환하는 값을 사용할 때일 수도 있다. 이런 것들을 우리가 정해줘야 한다.
- 포인트컷 : 부가기능의 적용위치
- 부가 기능을 만들고 언제 수행할지만 정하는 게 아니고 어디에! 수행할지도 정해야 한다.
- @Aspect
- Spring 빈(Bean) 클래스에만 적용 가능합니다.
- 어드바이스 종류
- @Around: '핵심기능' 수행 전과 후 (@Before + @After)
- @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
- @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
- @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
- @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
- 포인트컷
- 포인트컷 Expression 형태
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
- ? 는 생략 가능
- 포인트컷 Expression 예제
@Around("execution(public * com.sparta.myselectshop.controller..*(..))") public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }
- execution(modifiers-pattern? return-type-pattern declaring-type-pattern? **method-name-pattern(param-pattern)** throws-pattern?)
- modifiers-pattern
- public, private, *
- return-type-pattern
- void, String, List<String>, *****
- declaring-type-pattern
- 클래스명 (패키지명 필요)
- com.sparta.myselectshop.controller.* - controller 패키지의 모든 클래스에 적용
- com.sparta.myselectshop.controller.. - controller 패키지 및 하위 패키지의 모든 클래스에 적용
- method-name-pattern(param-pattern)
- 함수명
- addFolders : addFolders() 함수에만 적용
- add* : add 로 시작하는 모든 함수에 적용
- 파라미터 패턴 (param-pattern)
- (com.sparta.myselectshop.dto.FolderRequestDto) - FolderRequestDto 인수 (arguments) 만 적용
- () - 인수 없음
- (*) - 인수 1개 (타입 상관없음)
- (..) - 인수 0~N개 (타입 상관없음)
- 함수명
- @Pointcut
- 포인트컷 재사용 가능
- 포인트컷 결합 (combine) 가능
@Component @Aspect public class Aspect { @Pointcut("execution(* com.sparta.myselectshop.controller.*.*(..))") private void forAllController() {} @Pointcut("execution(String com.sparta.myselectshop.controller.*.*())") private void forAllViewController() {} @Around("forAllContorller() && !forAllViewController()") public void saveRestApiLog() { ... } @Around("forAllContorller()") public void saveAllApiLog() { ... } }
- 포인트컷 Expression 형태
개인과제
일단 시작은 했다..
시작이라도 한 나에게 칭찬하며..
지난번 팀 프로젝트의 Repository를 가져와서 test라는 branch를 만들어서 작업 중이다.
그리고 시작하자마자 막혔다. 역시 난 멋져>< 시작하자마자 막히다닛.. ㅎ
면담(질문)
- Q : 지난 번 과제 피드백에서 Autowired는 예전 거라서 지양한다고 들었는데, 강의 테스트에서는 Autowired을 사용 중인데 상관없나요? 테스트도 생성자를 사용할 수 있나요?
- Autowired가 직관적이지 않아서 지양하지 않지만, 테스트는 굳이 어렵게 하지 않아도 되서 사용해도 상관없다.
테스트에 많은 리소스를 사용 안해도 된다. 가볍게 해도 된다.
- Autowired가 직관적이지 않아서 지양하지 않지만, 테스트는 굳이 어렵게 하지 않아도 되서 사용해도 상관없다.
- Q : SpringBootTest 이거하면 Mockito 없어도 되는 건가요? 강의를 보고 느낀 Mock, MockBean을 사용하는 걸로 봐서 있어야 된다고 판단했지만, 맞는 건지 궁금합니다!
- @SpringBootTest는 SpringBoot의 Bean을 올린다.
통합테스트에서는 Mockito가 없어도 되지만, aws 이런 걸 사용하는 것은 비용이라서 일부에 Mockito를 사용해서 Response만 받기도 한다.
- @SpringBootTest는 SpringBoot의 Bean을 올린다.
- Q : @Mock이 Repository이고 Service가 @MockBean인가?
- 결국은 둘다 같다. 차이는 MockBean은 Bean을 Mock한다.
- @Mock는 다른 자바에서도 업데이트가 빠르고
- @MockBena은 스프링프레임워크에서만..
오늘의 회고
- 12시간 중 얼마나 몰입했는가?
어제 밤을 세워서 일단 하긴 했는데..
얼마나 몰입하게 되었는지는 모르겠다.
- 오늘의 생각
목표한 강의도 다 완강하고 개인과제도 시작이라도 한 나 칭찬한다.
근데 이제 WIL을 오전에 작성하지 못했지 고건 칭찬 회수..
그리고 내가 원하는 부가적인? 분야는 어떤 것이지?
보안, 인공지능, 유지보수, 예외처리?
아직은 PM이 하는 일이 정확하게 무엇인지 모르지만.. PM을 하고 싶은 것 같아.
설계가 좋아서. 아직 뭣 모르고 할 수 있는 소리였다..
- 내일 학습할 것은 무엇인지
일단은 과제를 집중적으로 볼까 싶다.
필수구현의 절반을 하는 것이 내 목표!
아니다 목표는 크게 잡으라고 했으니까 필수구현 완료까지...
728x90
반응형