본문 바로가기

개발/Spring

[Spring] 단위 테스트

단위 테스트 ?

  단위 테스트는 작은 단위로 쪼개서 각 단위가 정확하게 동작하는지를 검사하는 테스트 기법이다. 버그 발견 시간이 늦어질수록 해결비용이 기하급수적으로 늘어나기때문에 단위 테스트를 통해 문제가 발생하는 부분을 빠르고 정확하게 확인하는 과정이 필요하다.

 

 

JUnit5

  JUnit5는 자바 단위 테스트 프레임워크이다. 테스트 코드를 작성할 때 쓰이는 어노테이션들과 함수들을 몇개 정리해보려고 한다.

 

1. Befor / After test

  각각의 테스트 코드가 실행되기 전이나 후에 실행되는 함수들이다.

package com.sparta.junit5practice;

import org.junit.jupiter.api.*;

public class BeforeAfterTest {

    @BeforeEach
    void setUp() {
        System.out.println("각각의 테스트 코드가 실행되기 전에 수행");
    }

    @AfterEach
    void tearDown() {
        System.out.println("각각의 테스트 코드가 실행된 후에 수행\n");
    }

    @BeforeAll
    static void beforeAll() {
        System.out.println("모든 테스트 코드가 실행되기 전에 최초로 수행\n");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("모든 테스트 코드가 실행된 후 마지막으로 수행\n");
    }

    @Test
    void test1() {
        System.out.println("테스트 코드 작성 1");
    }

    @Test
    void test2() {
        System.out.println("테스트 코드 작성 2");
    }
}

 

위의 코드를 실행하면 아래와 같은 결과가 나오게 된다.

 

 

2. @Nested

: 테스트 코드를 클래스로 그룹지어서 사용할 때 사용된다. 이렇게 하면 그룹별로 파악하기에 좋다는 장점이 있다.

3. @DisplayName

: 주로 해당 테스트 코드가 어떤 테스트인지 이름을 지어줄 때 사용한다.

4. @Order

: 테스트 코드의 함수 사이에는 순서가 없고 동시다발적으로 테스트가 진행된다. 만약 순서를 부여하고 싶다면 Order를 사용해 순서를 부여해줄 수 있다.

 

 

@Nested / @DisplayName / @Order 사용 예시

@Nested
    @DisplayName("주제 별로 테스트를 그룹지어서 파악하기 좋습니다.")
    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    class Test1 {

        @Order(1)
        @Test
        @DisplayName("Test1 클래스")
        void test() {
            System.out.println("\nTest1 클래스");
        }

        @Order(3)
        @Test
        @DisplayName("Test1 - test1()")
        void test1() {
            System.out.println("Test1.test1");
        }

        @Order(2)
        @Test
        @DisplayName("Test1 - test2()")
        void test2() {
            System.out.println("Test1.test2");
        }
    }

 

 

5. @RepeatedTest

: test의 for문 ! 반복적으로 테스트할 때 사용한다. 지정해 준 value값만큼 반복하게 된다.

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

 

 

6. @ParameterizedTest / @ValueSource

: 파라미터를 활용하여 테스트 할 때 사용되는 어노테이션들이다.

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

 

 

7. AssertionTest

: 테스트 한 후 결과와 예측값이 맞는지 비교할 때 쓰이는 함수들을 정리해보았다.

public class AssertionTest {

    Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    @DisplayName("assertEquals")
    void test1() {
        Double result = calculator.operate(5, "/", 2);
        assertEquals(2.5, result);
    }

    @Test
    @DisplayName("assertEquals - Supplier")
    void test1_1() {
        Double result = calculator.operate(5, "/", 0);
        // 테스트 실패 시 메시지 출력 (new Supplier<String>())
        assertEquals(2.5, result, () -> "연산자 혹은 분모가 0이 아닌지 확인해보세요!");
    }

    @Test
    @DisplayName("assertNotEquals")
    void test1_2() {
        Double result = calculator.operate(5, "/", 0);
        assertNotEquals(2.5, result);
    }

    @Test
    @DisplayName("assertTrue 와 assertFalse")
    void test2() {
        assertTrue(calculator.validateNum(9));
        assertFalse(calculator.validateNum(0));
    }

    @Test
    @DisplayName("assertNotNull 과 assertNull")
    void test3() {
        Double result1 = calculator.operate(5, "/", 2);
        assertNotNull(result1);
        Double result2 = calculator.operate(5, "/", 0);
        assertNull(result2);
    }

    @Test
    @DisplayName("assertThrows")
    void test4() {
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> calculator.operate(5, "?", 2));
        assertEquals("잘못된 연산자입니다.", exception.getMessage());
    }
}

 

 

 

Mockito

Mockito를 통해 가짜 객체(Mock object)를 생성할 수 있다. 만약 Service 클래스만 테스트하고 싶다면 실제로는 Controller와 연관이 있지만 가짜 객체를 통해 분리 시켜서 Service만 테스트할 수 있게 해주어야한다. 이때 사용되는 것이 Mockito이다. 가짜 객체를 생성시키는 것이기때문에 CRUD작업을 테스트하더라도 실제 DB에 영향을 주진 않는다.

 

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

@Mock //가짜 객체를 생성하기 위해 사용된다.

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

    @Test
    @DisplayName("관심 상품 희망가 - 최저가 미만으로 변경")
    void test2() {
        // given
        Long productId = 200L;
        int myprice = ProductService.MIN_MY_PRICE - 50;

        ProductMypriceRequestDto requestMyPriceDto = new ProductMypriceRequestDto();
        requestMyPriceDto.setMyprice(myprice);

        ProductService productService = new ProductService(productRepository, folderRepository, productFolderRepository);

        // when
        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            productService.updateProduct(productId, requestMyPriceDto);
        });

        // then
        assertEquals(
                "유효하지 않은 관심 가격입니다. 최소 " +ProductService.MIN_MY_PRICE + "원 이상으로 설정해 주세요.",
                exception.getMessage()
        );
    }
}