[Spring#35] 단위 테스트, 통합테스트, Mockito, Spring AOP, API 예외처리와 에러메시지 / 알고리즘 : 가장 가까운 같은 글자

김한준 Hanjun Kim·2023년 11월 29일
0

내일배움캠프

목록 보기
36/70

알고리즘

깃허브 링크 : https://github.com/wkdehf217/codingTest/tree/main/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4/1/142086.%E2%80%85%EA%B0%80%EC%9E%A5%E2%80%85%EA%B0%80%EA%B9%8C%EC%9A%B4%E2%80%85%EA%B0%99%EC%9D%80%E2%80%85%EA%B8%80%EC%9E%90

  • 문자열을 잘라서 배열에 넣는다

  • 빈 리스트에 위의 문자가 없다면 문자를 추가하고, -1을 반환

  • 리스트에 위의 문자가 있다면, 그 문자의 위치를 얻어오고 나서 리스트 안에 있는 문자를 의미없는(aa)로 바꿔준 뒤, 리스트에 찾아온 문자를 넣어준다.

이런 방식으로 풀이했다.


단위 테스트 + JUnit5 + 테스트 기법

  • 단위 테스트 : 작은 단위로 쪼개서 정확하게 동작하는지 확인하는 테스트

    • 예 : 계산기의 메서드 하나하나 쪼개서 테스트
  • myselectshop 말고 JUnit5-practice에서 연습

  • @BeforeEach : "각각의 테스트 코드가 실행되기 전에 수행되는 메서드"

  • @AfterEach : "각각의 테스트 코드가 실행된 후에 수행"

  • @BeforeAll : "모든 테스트 코드가 실행되기 전에 최초로 수행"

  • @AfterAll : "모든 테스트 코드가 실행된 후에 마지막으로 수행"

  • @Test : 테스트 코드
    실행하는 방법은 테스트 클래스쪽에 >> 누르기

  • @DisplayName("") : 테스트의 내용을 한눈에 알아볼 수 있게 네이밍 해줄 때
    = 메서드 이름을 신경 쓸 필요 없음

  • @Nested : "주제 별로 테스트를 그룹지어서 파악하기 좋습니다."

  • @Order : 메서드 단위로 순서를 매길 때

  • @RepeatedTest : 메서드를 반복해서 테스트 = for문
    value : 5번 / name : 네이밍

  • @ParameterizedTest : "파라미터 값 활용하여 테스트 하기"
    전달되는 파라미터 수 만큼 실행이 됨 = 리스트같이

  • @Assertions : a와 b 비교 메서드. 검증
Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }
  • @BeforeEach를 통해서
    각 테스트가 실행되기 전 위에있는 new Calculator를 수행함으로써
    매번 새로운 객체가 생성된다

    • assertEquals - Supplier : 메시지 보내주기
    • assertTrue : Boolean 체크
    • assertNotNull : null 체크
    • assertThrows : exception 던지기
      • 예외 메시지가 맞는지 체크까지 가능
  • Given-When-Then 패턴

    • 테스트 코드 작성 패턴
    • Given : 테스트 하고자 하는 대상을 실제로 실행하기 전에 그 테스트에 필요한 값들을 미리 선언
    • When : 테스트 하고자 하는 대상을 실행시킴
    • Then : 어떤 특정한 행동 때문에 발생할 거라고 예상되는 결과에 대해 예측하고 맞는지 확인

      !주석 달기

    • When-Then : 실행과 예측을 동시에

Mockito

  • myselectshop productService쪽 수정

  • 원래 Service를 실행하려면 Repository나 다른것들이 필요한데 그것들이 없기 때문에 오류가 나고 있다.(findById 등)

    • 우리는 findById가 동작하는지가 궁금하는게 아니라
    • 업데이트를 잘 하는지가 궁금한 것
      -> Mock("가짜 객체")을 사용한다
  • @ExtendWith(MockitoExtension.class) // @Mock 사용을 위해 설정합니다.

    • 이후 @Mock으로 가짜 인터페이스들을 선언한다.
    • Given-When-Then 패턴 사용
  • 수행하면 오류가 난다.

    • 이유는? productId = 100 이라는 가짜 객체가 없기 때문에.
      • findById를 통해서 가져오는 상품도 직접 만들어서 넣어줘야 하기 때문(사용케이스 추가)
    • product 실제로 하나 만듬
    • given을 통해서 넣어줄 값을 안에 넣어준다
      • given(productRepository.findById(productId)).willReturn(Optional.of(product));
  • test2()는 "관심 상품 희망가 - 최저가 이상으로 변경" 인데,
    if() 부분인 최소값 체크하는 기능만 테스트 하기를 원하기 때문에
    given을 쓸 필요가 없다.

통합 테스트(@SpringBootTest)

  • 단위 테스트 VS 통합 테스트

    • 단위 테스트 : 세밀한 부분까지 테스트 가능 / 모듈 간의 상호 작용 검증 X
    • 통합 테스트 : 두 개 이상의 모듈이 연결된 상태 테스트 / 모듈 간의 연결에서 발생하는 에러 검증 O
  • 단위 테스트 시 Spring은 동작 x

  • @SpringBootTest

    • 테스트 수행 시 스프링이 동작
  • 테스트들은 테스트 동작 환경이 따로 있으며, 서로 영향이 받지 않도록 다 따로 동작한다
    = 필드들이 공유가 되지 않는다.

-> @TestInstance(TestInstance.Lifecycle.PER_CLASS) // 테스트 인스턴스의 생성 단위를 클래스로 변경합니다.

= 필드가 공유된다.

Controller 테스트

  • Security를 같이 사용하고 있기 때문에 까다롭다

    • SecurityContextHolder : 인증 객체를 담고있는, Context를 담는 공간
      • .setAuthentication : 인증객체 주기
      • 가짜 인증을 하는 행위(가짜 필터 만듬)
  • @WebMvcTest : Controller 테스트

    • 테스트할 Controller 지정
    • excludeFilters : 제외할 필터
  • MockMvc, 가짜 Principal도 선언 해준다.

  • .andExpect(view().name("login")) : 반환되는 페이지 Html이 login인지

  • Test는 Repository, 즉 jpa가 필요 없는데 @EnableJpaAuditing가 방해하기 때문에 오류가 남
    -> JpaConfig를 만들어서 해결 / @EnableJpaAuditing와 @Configuration 달아준다

  • Controller는 Security도 엮여있고 다른것들도 많이 엮여있어서 가짜로 만들어줘야할 것도 많다
    -> 나중에 왜 방해하는지, 안에 메서드들이 어떤건지 알아보기

    • Controller 테스트 하려면 WebMvc 만들기

    • Security를 사용하니까 가짜 Security 만들기

    • MockMvc를 사용해서 Http 요청을 만들 수 있는데 mvc는 setup() - builder에서 만듬

    • 가짜 필터도 넣어야 함

    • Jpa가 방해하니까 JpaConfig만들기

이정도만 이해하고 넘어가기

  • test2 - is3xxRedirection : redirect할때 3으로 시작하기 때문에(filter에서)

  • test3 - writeValueAsString : 클래스(ProductRequestDto)를 String타입으로 바꾸는것
    클라이언트는 Json 서버는 String이 필요하기 때문에

  • Service 테스트 : Mockito
    Controller 테스트 : WebMvcTest
    모듈연결 테스트 : SpringBootTest

  • @MockBean : UserController에는 UserService / FolderService / KakaoService 를 Bean 주입받아야 하는데
    이 어노테이션을 사용해서 가짜 Bean을 생성해 준다는 의미

MySelectShop Top5 회원

  • 목적 : 수행시간 측정하는 코드

    • File - New - Scratch file - java
  • 프로젝트 컨트롤러에 들어갔다가 나온 시간 측정

  • ApiUseTime이라는 Entity 생성해서 관리

    • Repository도 만듬
    • ProductController의 createProduct 코드 변경
  • 부가 기능 모듈화

    • 핵심 기능 말고 방금과같이 접속시간 체크같은 기능을 회원들이 알 필요 있을까?
    • 없다
    • 모든 핵심기능에 부가기능을 추가해야 한다면?
    • 핵심 기능이 나중에 추가된다면?
    • 핵심 기능이 수정된다면?
      • = AOP (부가 기능 모듈화)

Spring AOP

  • 부가기능과 핵심기능

  • 어드바이스 : "언제" 수행할 건지 정하는 것

    • 핵심 기능 전 / 후 / 전,후 전부 / 값을 반환했는데 사용할거냐 / 오류가 난다면 그때 수행을 할거냐
    • 정해줘야 함
  • 포인트컷 : 위치. "어디에" 수행할건지

  • @Aspect

    • AOP 설정 할거다!
  • 어드바이스 종류

    • @Around : 전+후
    • @Before : 전
    • @After : 후
    • @AfterReturning : 호출 성고 시(메서드의 Return값 사용가능)
  • @AfterThrouwing : 호출 실패 시(예외가 발생한 경우)

  • 포인트컷 사용법

    • execution(public com.sprata.myselectshop.controller..(..))

    • ?는 생략 가능하다는 뜻

      • com.springex.service
        com.springex.service 패키지만 선택

        	com.springex.service..
        	com.springex.service 패키지와 하위 패키지까지 모두 선택
        
        	com.springex.service..member
        	com.springex.service 패키지의 하위 패키지 중 member인 패키지

    훨씬 더 많지만 검색해서 찾아보자

  • 다 따라다니면서 넣어줄 순 없으니 @Pointcut을 통해 포인트컷 재사용 가능

  • 예시)

@Pointcut("execution(* com.sparta.myselectshop.controller.ProductController.*(..))")
    private void product() {}

@Around("product() || folder() || naver()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
	...
}

// 핵심기능 수행
Object output = joinPoint.proceed();

  • 프록시 : 대리인. 가짜
    • Spring이 프록시 객체를 중간에 삽입해준다.
    • DispatcherServlet과 ProductController입장은 변화가 없다.
    • input, output이 완전 동일하지만
    • joinPoint.proceed()에 의해 원래 호출하려고 했던 함수, 인수가 전달된다.
    • @Transactional 도 비슷한 경우

API 예외처리

  • 에러코드 200 번대 : 성공
    에러코드 400 번대 : 클라이언트 에러
    에러코드 500 번대 : 서버 에러

  • 에러의 정확 응답을 위해 ResponseEntity 클래스 사용해서 에러메시지 반환 <- 오늘뭐먹지 프로젝트때 비슷하게 해본것!

    • AOP기능이 동작하고 있는 에너테이션 사용하면 더 쉽게 사용 가능 = @ExceptionHandler
    • 그러면 프로젝트때 한 코드 이걸로 바꿀 수 있을듯?
@ExceptionHandler({IllegalArgumentException.class})
    public ResponseEntity<RestApiException> handleException(IllegalArgumentException ex) {
        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.BAD_REQUEST.value());
        return new ResponseEntity<>(
                // HTTP body
                restApiException,
                // HTTP status code
                HttpStatus.BAD_REQUEST
        );
    }
  • @ExceptionHandler 가 붙어있는 메서드는 해당 컨트롤러에서 에러가 발생하면 실행됨.

Spring의 Global 에러 처리

  • 매 Controller마다 해주는게 아니라 전체적으로 달아주기

  • @RestControllerAdvice : 모든 Controller에서 발생하는 에러를 다 가져올 수 있음.
    "클래스" 단위의 어노테이션이다.

  • Controller + ResponseBody = RestController
    ExceptionHandler + ResponseBody = RestControllerAdvice

Error 메시지 관리하는 법

  • Spring의 properties 파일을 통해 관리 할 수 있다.

    • 원하는 메시지들을 넣어놓고 사용함
    • resources에 추가, 뒤에 s 붙이기
  • 사용하는 클래스에서 MessageSource 클래스 사용해서 가져옴

    • new Integer[]{MIN_MY_PRICE}, : 배열 형태의 값 전달.
    • Locale.getDefault() : 기본 언어 설정
  • Exception클래스는 번개표시로 바뀐다

    • ProductNotFoundException이라는 커스텀 클래스를 만들어서,
      오류메시지를 던져줄 수 있다.
    • 배열 형태의 값 전달할 때 null 줘도 됌
    • 커스텀한 Exception 클래스도 GlobalExceptionHandler에서 잡아서 메시지 반환 할 수 있다.
@ExceptionHandler({ProductNotFoundException.class})
    public ResponseEntity<RestApiException> notFoundProductExceptionHandler(ProductNotFoundException ex) {
        RestApiException restApiException = new RestApiException(ex.getMessage(), HttpStatus.NOT_FOUND.value());
        return new ResponseEntity<>(
                // HTTP body
                restApiException,
                // HTTP status code
                HttpStatus.NOT_FOUND
        );
    }
profile
개발이 하고싶은 개발지망생

0개의 댓글