4 분 소요

스프링 복습 및 정리 14.2P

Hamcrest

Hamcrest는 JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework이다.

JUnit에서도 Assertion을 위한 다양한 메서드를 지원하지만

Hamcrest는 아래와 같은 이유로 JUnit에서 지원하는 Assertion 메서드보다 더 많이 사용된다.

  • Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어지므로 가독성이 향상된다.
  • 테스트 실패 메시지를 이해하기 쉽다.
  • 다양한 Matcher를 제공한다.

Hamcrest Assertion 적용하기

JUnit Assertion을 사용한 단위 테스트에 Hamcrest Assertion 적용하기

Junit → Hamcrest 1

JUnit

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

public class HelloJunitTest {
    @DisplayName("Hello Junit Test")
    @Test
    public void assertionTest1() {
        String actual = "Hello, JUnit";
        String expected = "Hello, JUnit";

        assertEquals(expected, actual);
    }
}

Hamcrest

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;

public class HelloHamcrestTest {

    @DisplayName("Hello Junit Test using hamcrest")
    @Test
    public void assertionTest1() {
        String expected = "Hello, JUnit";
        String actual = "Hello, JUnit";

        assertThat(actual, is(equalTo(expected)));  // (1)
    }
}

JUnit의 assertEquals(expected, actual);은 파라미터로 입력된 값의 변수 이름을 통해

대략적으로 어떤 검증을 하려는지 알 수 있으니 구체적인 의미는 유추를 하는 과정이 필요하다.

Hamcrest의 assertThat(actual, is(equalTo(expected)));는 assert that actual is equal to expected

라는 하나의 영어 문장으로 자연스럽게 읽힌다.

JUnit → Hamcrest 2

JUnit

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

public class HelloJunitTest {

    @DisplayName("Hello Junit Test")
    @Test
    public void assertionTest1() {
        String actual = "Hello, JUnit";
        String expected = "Hello, World";

        assertEquals(expected, actual);
    }
}

위 테스트 케이스 실행 결과는 failed이다. 아래는 실행 결과이다.

expected: <Hello, World> but was: <Hello, JUnit>
Expected :Hello, World
Actual   :Hello, JUnit

Hamcrest

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class HelloHamcrestTest {

    @DisplayName("Hello Junit Test using hamcrest")
    @Test
    public void assertionTest() {
        String expected = "Hello, World";
        String actual = "Hello, JUnit";

        assertThat(actual, is(equalTo(expected)));
    }
}

위 테스트 케이스 실행 결과 역시 failed이다. 아래는 실행 결과이다.

Expected: is "Hello, World"
     but: was "Hello, JUnit"

Hamcrest의 Matcher를 사용해서 사람이 읽기 편한 자연스러운 Assertion 문장을 구성할 수 있으며,

실행 결과가 failed일 경우 역시 자연스러운 failed 메시지를 확인할 수 있기 때문에 가독성이 높다.

JUnit → Hamcrest 3

테스트 대상 클래스

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

JUnit

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

public class AssertionNullHamcrestTest {

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

Hamcrest

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class AssertionNullHamcrestTest {

    @DisplayName("AssertionNull() Test")
    @Test
    public void assertNotNullTest() {
        String currencyName = getCryptoCurrency("ETH");

        assertThat(currencyName, is(notNullValue()));   // (1)
        // assertThat(currencyName, is(nullValue()));   // (2)
    }

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

Not Null 테스트를 하기 위해서는 (1)과 같이 Hamcrest의 is(), notNullValue() 매쳐를 함께 사용할 수 있다.

currencyName is not Null Value. 와 같이 가독성 좋은 하나의 문장처럼 구성이 되는 것을 볼 수 있다.

만약 (2)를 주석 해제하면, 아래와 같은 failed 메시지를 확인할 수 있다.

Expected: is null
     but: was "Ethereum"

JUnit → Hamcrest 4

JUnit

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

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

위 코드에서는 JUnit의 assertThrows()를 이용해서 XRP 암호 화폐가 map에 존재하는지(null이 아닌지)

여부를 던져지는 예외 발생 여부로 테스트하고 있다.

Hamcrest

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class AssertionExceptionHamcrestTest {

    @DisplayName("throws NullPointerException when map.get()")
    @Test
    public void assertionThrowExceptionTest() {
        Throwable actualException = assertThrows(NullPointerException.class,
                () -> getCryptoCurrency("XRP"));   // (1)

        assertThat(actualException.getClass(), is(NullPointerException.class));  // (2)
    }

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

위 코드에서는 발생한 예외가 NullPointerException인지 여부를 체크하고 있다.

그런데, 예외에 대한 테스트는 Hamcrest 만으로 Assertion을 구성하기 힘들기 때문에 (1)과 같이

JUnit의 assertThrows() 메서드를 이용해서 assertThrows()의 리턴 값을 전달받은 후에

(2)와 같이 assertThat(actualException.getClass(), is(NullPointerException.class)); 을 통해

throw 된 Exception 타입이 기대했던 Exception 타입과 일치하는지 추가로 검증을 진행했다.

만약 Hamcrest 만으로 던져진 예외를 테스트하기 위해서는 Custom Matcher를 직접 구현해서 사용할 수 있다.

위 코드의 실행 결과는 (1)에서 1차적으로 NullPointException이 발생하므로 (1)의 Assertion 결과는 passed 이고,

(2)에서 결과 값인 actualException.getCause()가 null 이므로, (2)의 Assertion 결과 역시 passed이다.

추가 자료

Hamcrest Matcher1 [더보기]

Hamcrest Custom Matcher 구현법 [더보기]

Spring Boot 핵심 정리 모음

1P - POJO, IoC, DI, AOP, PSA >

1.1P - DI / 의존성 주입은 왜 필요한가? >

2P - 빈(Bean) 생명주기와 범위 >

3P - 컴포넌트 스캔과 의존성 자동 주입, @Autowired >

4P - AOP (관심 지향 프로그래밍) >

5P - Spring MVC / MVC의 동작 방식과 구성 요소 >

6P - REST, REST API >

7P - HTTP Header, Rest Client >

8P - DTO, DTO 유효성 검증 >

9P - Service / Entity 클래스, Mapper / MapStruct >

10P - 예외 처리 (@ExceptionHandler, @RestControllerAdvice) >

10.1P - 비즈니스 로직에 대한 예외 처리 >

11P - JDBC 데이터 액세스 계층 >

11.1P - DDD, 도메인 엔티티 및 테이블 설계 >

11.2P - 데이터 액세스 계층 구현 (도메인 엔티티 클래스 정의) >

11.3P - 데이터 액세스 계층 구현 (서비스, 리포지토리 구현) >

12P - JPA(Java Persistence API) >

12.1P - JPA 엔티티 매핑 >

12.2P - JPA 엔티티 간의 연관 관계 매핑 >

12.3P - Spring Data JPA 데이터 액세스 계층 구현 >

13P - Spring MVC 트랜잭션 >

13.1P - 선언형 방식의 트랜잭션 적용 >

13.2P - JTA를 이용한 분산 트랜잭션 적용 >

14P - Spring MVC Testing 단위 테스트 >

14.1P - JUnit 단위 테스트 >

14.2P - Hamcrest Assertion >

14.3P - 슬라이스 테스트 (API, 데이터 액세스 계층) >

14.4P - Mockito >

14.5P - TDD (Test Driven Development) >

15P - API 문서화 (Documentation) >

15.1P - API 문서 생성, Asciidoc, Asciidoctor >

16P - 애플리케이션 빌드 / 실행 / 배포 >

17P - Spring Security >

17.1P - Spring Security 2 >

18P - JWT (JSON Web Token) >

댓글남기기