테스트 코드

ayboori·2023년 8월 12일
0

Spring

목록 보기
13/24

테스트 코드를 짜기 힘든 이유?

코드가 Testable 하게 짜여 있지가 않아서 어떻게 테스트를 할지 잘 모르기 때문이다!

Test - SOLID 원칙은 매우 밀접한 관계이다.

Full Coverage까진 못 가더라도, 보통 성공 케이스와 주요한 실패 케이스는 고려해서 작성하는 게 좋다. 안정성을 확보하는 데에 아주 중요한 역할을 한다!

단위 테스트

작은 단위로 쪼개서 각 단위가 정확하게 동작하는지를 검사
Edge 포인트를 주면서 메소드 하나 내에서도 여러 개의 테스트 코드를 수행해볼 수 있다

자바 프로그래밍 언어 용 단위 테스트 프레임워크 : JUnit5

Before - After

  • @BeforeEach : 각각의 테스트 코드가 실행되기 전에 수행
    @BeforeEach
    void setUp() {    }
  • @AfterEach : 각각의 테스트 코드가 실행된 후에 수행
    @AfterEach
    void tearDown() {    }
  • @BeforeAll : 모든 테스트 코드가 실행되기 전에 최초로 수행
    무조건 static으로 선언
    @BeforeAll
    static void beforeAll() {    }
  • @AfterAll : 모든 테스트 코드가 수행된 후 마지막으로 수행
    무조건 static으로 선언
    @AfterAll
    static void afterAll() {    }

테스트 꾸미기

  • @DisplayName("테스트 내용 알아보기 쉽게 네이밍")
  • @Nested : 주제 별로 테스트를 그룹 짓기
    - Nested 아래에 클래스, 내부에 test 메소드 넣기
  • @Order(순서) : 테스트 순서 지정

반복 테스트 (@RepeatedTest)

@RepeatedTest(value = 5, name = "반복 테스트 {currentRepetition} / {totalRepetitions}")
void repeatTest(RepetitionInfo info) {
    System.out.println("테스트 반복 : " + info.getCurrentRepetition() + " / " + info.getTotalRepetitions());
}

value = 총 테스트 돌려볼 횟수
{currentRepetition} / {totalRepetitions} : 현재 반복 횟수 / 총 횟수

파라미터 값 테스트 (@ParameterTest)

@DisplayName("파라미터 값 활용하여 테스트 하기")
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9})
void parameterTest(int num) {
    System.out.println("5 * num = " + 5 * num);
}

@ValueSource : 전달할 파라미터 값, 갯수 만큼 테스트한다

Assert 테스트

assertEquals(기댓값, 결과값 <을 담은 변수>) : 두 값이 일치하지 않을 시 오류
assertEquals(기댓값, 결과값 <을 담은 변수>, () -> "오류 시 던질 메시지")
assertNotEquals(참일 때 기댓값, 결과값 <을 담은 변수>) : 두 값이 일치할 시 오류

assertTrue(calculator.validateNum(9)); : return 값이 True인지 확인
assertFalse(calculator.validateNum(0)); : return 값이 False인지 확인

assertNotNull()
assertNull()

  • Exception
@Test
@DisplayName("assertThrows")
void test4() {
// 던진 오류를 담아서 변수에 담기
    IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> calculator.operate(5, "?", 2)); 
// 오류의 메시지가 예측값과 동일한지 확인    
    assertEquals("잘못된 연산자입니다.", exception.getMessage()); 
}

지정한 Exception 발생 되는지 확인한다.

테스트 패턴

given : 이런 상황 시 (미리 준비한 테이터)
when : 이런 호출을 하면 (함수 실행)
then : 이렇게 된다. (assert)

실패 시 예외 처리가 성공했는지도 확인 후 던져야 한다.

실행과 결과가 같이 나올 경우 when - then 으로 작성하기도 한다.

Mockito 테스트

Mock Object

  • Repository / Service 따로 테스트 하기 위해 사용한다.
  • 사용을 위해서는 Mockito를 적용해야 한다.

MockRepository

  • 실제 객체와 클래스명, 함수명 등 겉만 같은 객체이다.
  • 실제 DB 작업이 이루어지는 것은 아니고, 테스트를 위해 필요한 결과값을 return 한다.

1) Test 클래스명 위에 @ExtendWith(MockitoExtension.class)
2) @Mock 을 사용해서 Repository를 받아온다.

이때 하고 싶은 테스트가 update / save 등이고 repository에서 읽어오는 부분이 아니라면, repository에서 읽어오는 부분은 우리가 따로 명시해줘야 한다.

@ExtendWith(MockitoExtension.class) // @Mock 사용을 위해 설정합니다.
class ProductServiceTest {

    @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를 만들고, RequestDto에 값을 삽입 (빈 생성자가 아님)하는 아래의 과정을 직접 명시해줘야 한다.
// when 전까지만 다르고, 나머지 부분은 동일

        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());
    }

Product 객체를 만드는 부분은 우리가 테스트하고 싶은 부분이 아니기 때문에 직접 명시해준다.
이때 MyPrice는
given(값을 넣어줄 부분 코드.리턴 받아올 값)

즉 위의 코드에서는 productRepository.findById(productId) 수행 시 Optional.of(product)를 리턴하게 되는 것이다.

  • 하나의 메소드 내부에서도 엣지포인트가 달라지면 각각을 다 테스트 해야 한다. (assert를 여러개 쓸 수 있다는 뜻)

통합 테스트

  • 두 개 이상의 모듈이 연결된 상태를 테스트 할 수 있다
    - Controller / Service의 연결 등을 테스트 (Mock Object가 필요 없다!)


단위 테스트 > 통합 테스트 > UI 테스트
이때 단위 테스트의 비중이 80%에 가깝다!

@SpringBootTest 사용 시 스프링이 동작한다. (단위 테스트 시 동작 X)
즉, Spring IoC/DI 기능을 사용 가능 / Repository를 사용해 DB CRUD가 가능합니다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 서버의 PORT 를 랜덤으로 설정합니다.
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // 테스트 인스턴스의 생성 단위를 클래스로 변경합니다.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)

@TestInstance 사용 전에는 각각의 메소드가 따로 수행되며, 서로 영향을 끼치지 않는다. 전역 변수를 공유하지 않는다.
@TestMethodOrder : @Order을 사용하기 위해서 추가한다.
@SpringBootTest 사용 시 @AutoWired를 사용해서 Bean으로 주입 받아올 수 있다.

테스트를 통해 생성된 객체와, repository에서 찾아온 객체가 같은지 비교한다 (저장이 잘 되었는지 확인)

Controller 테스트

Mockup Filter 만들기

  • @WebMvcTest : 테스트 클래스 외부에서 테스트 할 컨트롤러를 지정, 제외할 필터를 지정
@WebMvcTest(
        controllers = {UserController.class, ProductController.class},
        excludeFilters = {
                @ComponentScan.Filter(
                        type = FilterType.ASSIGNABLE_TYPE,
                        classes = WebSecurityConfig.class
                )
        }
)

Mock 테스트를 위한 유저가 필요하다

mvc.perform

    @Test
    @DisplayName("로그인 Page")
    void test1() throws Exception {
        // when - then
        mvc.perform(get("/api/user/login-page")) // get 방식 URI
                .andExpect(status().isOk()) // 예상되는 상태 값
                .andExpect(view().name("login")) // 예상되는 view의 이름을 login으로 지정하기
                .andDo(print()); // 출력하기
    }

해당 컨트롤러가 URL을 잘 반환하는지 확인하는 역할이다.

Controller 테스트 시 @EnableJpaAuditing 때문에 오류 발생할 수 있다.

main 앞에 붙어있는 @EnableJpaAuditing을 삭제 후, 따로 Config 클래스 만들어서 거기에 달아 준다.
+) 해당 설정 활성화를 위해 @Configuraion 달아 준다.

ModelAndView ?

request 전달 방식

@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) 
                        // 위에서 만든 requestForm을 post에서 입력 받는다.
                )
                .andExpect(status().is3xxRedirection()) // redirect 타입이라 3으로 시작한다.
                .andExpect(view().name("redirect:/api/user/login-page")) // 반환할 url
                .andDo(print());
    }

requestForm.add에 key - value 형식으로 값을 입력한다.

writeValueAsString

    @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) // requestDto 타입
                        .accept(MediaType.APPLICATION_JSON)
                        .principal(mockPrincipal) // 위에서 받아온 mock 인증
                )
                .andExpect(status().isOk())
                .andDo(print());
    }

requestDto 값을 지정해서 request 값을 준다.

profile
프로 개발자가 되기 위해 뚜벅뚜벅.. 뚜벅초

0개의 댓글