컴공생의 발자취

[내일배움캠프 40일차 TIL] 통합 테스트와 Spring AOP(어드바이스, 포인트컷) 본문

🤝 활동/내배캠TIL

[내일배움캠프 40일차 TIL] 통합 테스트와 Spring AOP(어드바이스, 포인트컷)

MNY 2024. 6. 14. 01:18
728x90
반응형
2024.06.13.(목)
오늘의 진도 : Spring 심화주차 1주차 완강까지...

하.. 너무 더워... 아이스크림 마냥 녹아내린드아🍦
오늘의 학습, 개인과제, 면담, 오늘의 회고 이렇게 6개의 큰 틀로 나누어 정리할 것이다.

 

💡 오늘의 학습 키워드

- 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(어드바이스, 포인트컷)

 

  • 어드바이스 : 부가기능
    • 우리가 만든 부가기능을 핵심 기능 언제 수행할 건지를 정하는 것
    • 그 언제가 핵심 기능 전일 수도 후일 수도 있고 전, 후 전부일 수도 있고 핵심 기능이 오류가 발생했을 때 일 수도 있다. 혹은 반환하는 값을 사용할 때일 수도 있다. 이런 것들을 우리가 정해줘야 한다.
  • 포인트컷 : 부가기능의 적용위치
    • 부가 기능을 만들고 언제 수행할지만 정하는 게 아니고 어디에! 수행할지도 정해야 한다.

 

  1. @Aspect
    • Spring 빈(Bean) 클래스에만 적용 가능합니다.
  2. 어드바이스 종류
    • @Around: '핵심기능' 수행 전과 후 (@Before + @After)
    • @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
    • @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
    • @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
    • @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
  3.  포인트컷
    • 포인트컷 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() {
        		...
        	}	
        }

개인과제

일단 시작은 했다..

시작이라도 한 나에게 칭찬하며.. 

 

지난번 팀 프로젝트의 Repository를 가져와서 test라는 branch를 만들어서 작업 중이다.

그리고 시작하자마자 막혔다. 역시 난 멋져>< 시작하자마자 막히다닛.. ㅎ

 


면담(질문)

  • Q : 지난 번 과제 피드백에서 Autowired는 예전 거라서 지양한다고 들었는데, 강의 테스트에서는 Autowired을 사용 중인데 상관없나요? 테스트도 생성자를 사용할 수 있나요?
    • Autowired가 직관적이지 않아서 지양하지 않지만, 테스트는 굳이 어렵게 하지 않아도 되서 사용해도 상관없다.
      테스트에 많은 리소스를 사용 안해도 된다. 가볍게 해도 된다.
  • Q : SpringBootTest 이거하면 Mockito 없어도 되는 건가요? 강의를 보고 느낀 Mock, MockBean을 사용하는 걸로 봐서 있어야 된다고 판단했지만, 맞는 건지 궁금합니다!
    • @SpringBootTest는 SpringBoot의 Bean을 올린다.
      통합테스트에서는 Mockito가 없어도 되지만, aws 이런 걸 사용하는 것은 비용이라서 일부에 Mockito를 사용해서 Response만 받기도 한다.
  • Q : @Mock이 Repository이고 Service가 @MockBean인가?
    • 결국은 둘다 같다. 차이는 MockBean은 Bean을 Mock한다.
    • @Mock는 다른 자바에서도 업데이트가 빠르고
    • @MockBena은 스프링프레임워크에서만..

 


오늘의 회고

  • 12시간 중 얼마나 몰입했는가?

어제 밤을 세워서 일단 하긴 했는데..

얼마나 몰입하게 되었는지는 모르겠다.

 

  • 오늘의 생각

목표한 강의도 다 완강하고 개인과제도 시작이라도 한 나 칭찬한다.

근데 이제 WIL을 오전에 작성하지 못했지 고건 칭찬 회수..

 

그리고 내가 원하는 부가적인? 분야는 어떤 것이지?

보안, 인공지능, 유지보수, 예외처리?

아직은 PM이 하는 일이 정확하게 무엇인지 모르지만.. PM을 하고 싶은 것 같아.

설계가 좋아서. 아직 뭣 모르고 할 수 있는 소리였다..

 

  • 내일 학습할 것은 무엇인지

일단은 과제를 집중적으로 볼까 싶다.

필수구현의 절반을 하는 것이 내 목표!

아니다 목표는 크게 잡으라고 했으니까 필수구현 완료까지...

728x90
반응형