티스토리 뷰

728x90
반응형

지난 글에선 테스트의 종류와 단위 테스트, JUnit 없는 단위 테스트 구현을 살펴보았다.

 

2022.09.07 - [개발/Spring] - [Spring]단위 테스트(Unit Test)

 

[Spring]단위 테스트(Unit Test)

지난 글까진 만들어진 애플리케이션에 트랜잭션을 적용해 보았다. 2022.09.05 - [개발/Spring] - [Spring]트랜잭션(Transaction) 2022.09.06 - [개발/Spring] - [Spring]트랜잭션(Transaction) - @Transactional [..

gnidinger.tistory.com

이번 글에선 JUnit에 대해 알아보고, 이전 글에서 작성한 코드에 JUnit을 적용해 보자.

 

JUnit

 

JUnit은 스프링에서 지원하는 많은 테스팅 프레임워크 중 하나로, 1997년 Kent Beck에 의해 태어났다.

 

2022년 기준으로 가장 많이 사용되는 프레임워크이며 다양한 라이브러리를 지원한다.

 

Spring Boot Initializr에서 Gradle 기반의 프로젝트를 생성하면 기본적으로 테스트 패키지가 생성되며, JUnit을 포함한

testImplementation 'org.springframework.boot:spring-boot-starter-test'

의존성이 build.gradle 안에 자동으로 추가된다.

 

따라서 별도의 설정 없이 코드 작성을 시작할 수 있다.

 

Basic Structure of a JUnit Test

 

JUnit 테스트의 기본 구조는 굉장히 간단하다.

import org.junit.jupiter.api.Test;

public class JunitDefaultStructure {
    
    @Test
    public void test1() {
    }
    
    @Test
    public void test2() {
    }
    
    @Test
    public void test3() {
        
    }
}

테스트 하고자 하는 메서드에 @Test 애너테이션을 추가한 뒤 테스트 로직을 작성하면 된다.

 

Using an Assertion Method

 

Assertion은 특정 포인트에 대한 boolean 값이며, 테스트의 결과를 참/거짓으로 나타내는 논리적 표현이라고 했었다.

 

이전 글에선 Assertion 로직을 손으로 직접 입력했지만,

 

JUnit에선 자체적으로 Assertion을 위한 다양한 라이브러리를 지원한다.

 

하나씩 살펴보자.

 

  • assertEquals()
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class HelloJUnitTest {
    @DisplayName("Hello JUnit Test")
    @Test
    public void assertionTest() {
        String expected = "Hello, JUnit";
        String actual = "Hello, JUnit";
        
        assertEquals(expected, actual);
    }
}

assertEquals()두 대상이 일치하는지 검증하는 메서드이다.

 

위에 말했던 것처럼 @Test를 추가했고 @DisplayName()을 통해 테스트 결과의 이름을 지정한 것을 볼 수 있다.

 

해당 케이스를 실행하면 아래와 같은 결과를 얻을 수 있다.

 

만약 두 값이 다르다면 아래와 같이 테스트가 실패하며, 설명과 함께 테스트가 종료된다.

 

 

  • assertionNotNull()
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertNotNull;

public class AssertionNotNullTest {
    
    @DisplayName("AssertionNotNull() Test")
    @Test
    public void assertNotNullTest() {
        String currencyName = getCryptoCurrency("ETH");
        
        assertNotNull(currencyName, "should be not null");
    }
    
    private String getCryptoCurrency(String unit) {
        return CryptoCurrency.map.get(unit);
    }
}

assertionNotNull()은 이름 그대로 대상이 null인지 아닌지 검증한다.

 

대상이 null이 아닐 경우 통과되며, null일 경우 실패한다.

 

테스트 대상 객체와 실패 메시지까지 입력한 것을 확인할 수 있다.

 

실행 결과는 CryptoCurrency 클래스에 ETH가 있기 때문에 성공한다.

 

CryptoCurrency 클래스는 아래와 같이 생성했다.

import java.util.HashMap;
import java.util.Map;

public class CryptoCurrency {
    public static Map<String, String> map = new HashMap<>();

    static {
        map.put("BTC", "Bitcoin");
        map.put("ETH", "Ethereum");
        map.put("ADA", "ADA");
        map.put("POT", "Polkadot");
    }
}

 

  • assertThrows()
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertThrows;

public class AssertionExceptionTest {
    
    @DisplayName("throws NullPointerException when map.get()")
    @Test
    public void assertionThrowExceptionTest() {
        assertThrows(NullPointerException.class, () -> getCryptoCurrency("XRP"));
    }
    
    private String getCryptoCurrency(String unit) {
        return CryptoCurrency.map.get(unit).toUpperCase();
    }
}

assertThrows()는 대상 메서드 호출시 특정 Exception이 발생하는지 검증한다.

 

특정 Exception이 발생하면 통과, 그렇지 않으면 실패하게 된다.

 

위와 같이 발생이 기대되는 예외 클래스(NullPointerException.class)를 입력한 뒤 테스트 대상 메서드를 호출한다.

 

참고로 대상 메서드를 호출하는 함수형 인터페이스는 JUnit에서 지원하는 Executable이다.

 

호출된 getCryptoCurrency()는 매개변수를 찾은 뒤 대문자로 전환하려 하지만 반환 값이 null이기 때문에

 

NullPointerException이 발생하게 된다.

 

결과는 위와 같이 성공인 것을 확인할 수 있다.

 

여기서 만약 NullPointerException.class가 아닌 다른 타입의 예외를 지정하면 어떻게 될까?

 

정답은 위와 같다.

 

@BeforeEach, @BeforeAll

 

@BeforeEach와 @BeforeAll은 테스트에 앞서 초기화 등 필요한 전처리를 할 때 사용된다.

 

  • @BeforeEach
public class BeforeEachTest1 {

    @BeforeEach
    public void init() {
        System.out.println("Pre-processing before each test case");
    }
    
    @DisplayName("@BeforeEach Test 1")
    @Test
    public void beforeEachTest1() {
    }
    
    @DisplayName("@BeforeEach Test 2")
    @Test
    public void beforeEachTest2() {
    }
}

@BeforeEach 애너테이션이 추가된 메서드는 각 테스트 케이스 실행 직전에 먼저 실행된다.

 

실행 로그를 보면 두 번 실행된 것을 확인할 수 있다.

public class BeforeEachTest2 {
    public Map<String, String > map;

    @BeforeEach
    public void init() {
        map = new HashMap<>();
        map.put("BTC", "Bitcoin");
        map.put("ETH", "Ethereum");
        map.put("ADA", "ADA");
        map.put("POT", "Polkadot");
    }

    @DisplayName("Test case 1")
    @Test
    public void beforeEachTest1() {
        map.put("XRP", "Ripple");
        assertDoesNotThrow(() -> getCryptoCurrency("XRP"));
    }

    @DisplayName("Test case 2")
    @Test
    public void beforeEachTest2() {
        System.out.println(map);
        assertDoesNotThrow(() -> getCryptoCurrency("XRP"));
    }

    private String getCryptoCurrency(String unit) {
        return map.get(unit).toUpperCase();
    }
}

그렇다면 위와 같은 코드를 실행 시키면 어떻게 될까?

 

assertDoesNotThrow() 메서드는 예외가 발생하지 않는다고 기대하는 역할을 한다.

 

여기선 각 테스트 케이스 직전에 map을 초기화하기 때문에 테스트 1에서 추가한 요소가 테스트 2에선 존재하지 않는다.

 

따라서 위와 같이 테스트 케이스 2는 실패하게 된다.

 

콘솔에 출력된 요소 안에 XRP가 없는 것도 확인할 수 있다.

 

  • @BeforeAll
public class BeforeAllTest {
    public static Map<String, String > map;

    @BeforeAll
    public static void initAll() {
        map = new HashMap<>();
        map.put("BTC", "Bitcoin");
        map.put("ETH", "Ethereum");
        map.put("ADA", "ADA");
        map.put("POT", "Polkadot");
        map.put("XRP", "Ripple");

        System.out.println("Initialize Crypto Currency Map");
    }

    @DisplayName("Test case 1")
    @Test
    public void beforeAllTest1() {
        assertDoesNotThrow(() -> getCryptoCurrency("XRP"));
    }

    @DisplayName("Test case 2")
    @Test
    public void beforeAllTest2() {
        assertDoesNotThrow(() -> getCryptoCurrency("ADA"));
    }

    private String getCryptoCurrency(String unit) {
        return map.get(unit).toUpperCase();
    }
}

@BeforeAll은 클래스 레벨에서 테스트 실행시 딱 한 번만 실행되는 초기 작업 애너테이션이다.

 

@BeforeEach와는 다르게 반드시 static Method에만 붙여야 한다는 특징이 있다.

 

두 개의 실행 결과 모두 성공이며, 콘솔에 메시지가 한 번만 출력된 것을 확인할 수 있다.

 

방금 알아본 전처리와 마찬가지로 @AfterBefore, @AfterAll을 이용해 테스트의 후처리를 할 수 있으며,

 

호출 시점 외에 사용 방법은 동일하다.

 

Assumption

 

Assumption은 특정 상황에서만 테스트 케이스를 실행할 수 있도록 도와주는 라이브러리이다.

public class AssumptionTest {
    @DisplayName("Assumption Test")
    @Test
    public void assumptionTest() {
//        assumeTrue(System.getProperty("os.name").startsWith("Windows"));
        assumeTrue(System.getProperty("os.name").startsWith("Mac"));

        System.out.println("execute?");
        assertTrue(processOnlyMacTask());
    }

    private boolean processOnlyMacTask() {
        return true;
    }
}

테스트 케이스를 실행하는 OS에 따라 실행 여부를 결정하는 코드이다.

 

나는 macOS 환경을 사용하고 있으므로 아래와 같은 결과를 얻을 수 있다.

 

 

A Unit Test with JUnit

 

지난 글에서 JUnit을 적용하지 않고 헬퍼 클래스에 대한 단위 테스트를 진행했다.

public class StampCalculatorTestWithoutJUnit {
    public static void main(String[] args) {
        calculateStampCountTest();
    }

    private static void calculateStampCountTest() {
        // given
        int nowCount = 5;
        int earned = 3;

        // when
        int actual = StampCalculator.calculateStampCount(nowCount, earned);

        int expected = 7;

        // then
        System.out.println(expected == actual);
    }

    private static void calculateEarnedStampCountTest() { // 추가
        // given
        Order order = new Order();
        OrderCoffee orderCoffee1 = new OrderCoffee();
        orderCoffee1.setQuantity(3);

        OrderCoffee orderCoffee2 = new OrderCoffee();
        orderCoffee2.setQuantity(5);

        order.setOrderCoffees(List.of(orderCoffee1, orderCoffee2));

        int expected = orderCoffee1.getQuantity() + orderCoffee2.getQuantity();

        // when
        int actual = StampCalculator.calculateEarnedStampCount(order);

        // then
        System.out.println(expected == actual);
    }
}

스탬프를 합산하는 메서드와 주문 정보에서 스탬프 개수를 얻어내 합산하는 메서드를 검증하는 로직이다.

 

위와 같은 로직에 위에서 확인한 JUnit을 적용하면 아래와 같이 간결한 코드를 얻을 수 있다.

public class StampCalculatorTest {
    @Test
    @DisplayName("Test 1: 스탬프 카운트 계산 단위 테스트")
    public void calculateStampCountTest() {
        int nowCount = 5;
        int earned = 3;
        int actual = StampCalculator.calculateStampCount(nowCount, earned);
        int expected = nowCount + earned;

        assertEquals(expected, actual);
    }

    @Test
    @DisplayName("Test 2: 주문 후 누적 스탬프 카운트 계산 단위 테스트")
    public void calculateEarnedStampCountTest(){
        Order order = new Order();
        OrderCoffee orderCoffee1 = new OrderCoffee();
        orderCoffee1.setQuantity(2);
        OrderCoffee orderCoffee2 = new OrderCoffee();
        orderCoffee2.setQuantity(3);
        order.setOrderCoffees(List.of(orderCoffee1, orderCoffee2));

        int expected = orderCoffee1.getQuantity() + orderCoffee2.getQuantity();
        int actual = StampCalculator.calculateEarnedStampCount(order);

        assertEquals(expected, actual);
    }
}

성공적으로 테스트를 통과하는 것을 확인할 수 있다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함