2022. 7. 13. 12:47ㆍ카테고리 없음
Hamcrest는 JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework입니다.
JUnit에서도 Assertion 을 위한 다양한 메서드를 지원하지만 Hamcrest는 다음과 같은 이유로 JUnit에서 지원하는 Assertion 메서드보다 더 많이 사용됩니다.
Hamcrest를 사용하는 이유
- Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어지므로 가독성이 향상 된다.
- 테스트 실패 메시지를 이해하기 쉽다.
- 다양한 Matcher를 제공한다.
JUnit Assertion을 사용한 단위 테스트에 Hamcrest Assertion 적용해보기
Junit → Hamcrest 예 1
✅ JUnit에서의 Assertion
public class HelloJunitTest {
@DisplayName("Hello Junit Test")
@Test
public void assertionTest1() {
String actual = "Hello, JUnit";
String expected = "Hello, JUnit";
assertEquals(expected, actual); // (1)
}
}
HelloJunitTest 클래스를 Hamcrest의 Matcher를 이용해서 검증을 해보도록 하겠습니다.
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)
}
}
코드 3-191에서 JUnit의 Assertion 메서드와 코드 3-192의 (1)과 같이 Hamcrest의 매쳐(Matcher)를 사용했을 때의 코드를 비교해 보겠습니다.
- JUnit Assertion 기능 이용
- assertEquals(expected, actual);
- 파라미터로 입력된 값의 변수 이름을 통해 대략적으로 어떤 검증을 하려는지 알 수 있으나 구체적인 의미는 유추를 하는 과정이 필요합니다.
- assertEquals(expected, actual);
- Hamcrest의 매쳐(Matcher) 이용
- assertThat(actual, is(equalTo(expected)));
- (1)의 Assertion 코드 한 줄은 ‘assert that actual is equal to expected’라는 하나의 영어 문장으로 자연스럽게 읽혀집니다.
- 굳이 한글로 번역하자면, ‘결과 값(actual)이 기대 값(expected)과 같다는 것을 검증(Assertion)한다.’ 정도로 해석할 수 있습니다.
- assertThat() 메서드의 파라미터
- 첫 번째 파라미터는 테스트 대상의 실제 결과 값입니다.
- 두 번째 파라미터는 기대하는 값입니다. 즉, 이런 값일거라고 기대(예상)하는 값입니다.
- (1)의 Assertion 코드 한 줄은 ‘assert that actual is equal to expected’라는 하나의 영어 문장으로 자연스럽게 읽혀집니다.
- assertThat(actual, is(equalTo(expected)));
Junit → Hamcrest 예 2
✅ JUnit에서의 Assertion
public class HelloJunitTest {
@DisplayName("Hello Junit Test")
@Test
public void assertionTest1() {
String actual = "Hello, JUnit";
String expected = "Hello, World";
assertEquals(expected, actual);
}
}
[코드 3-193] JUnit Assertion에서의 “failed” 메시지 예
에러에러에러에러에러에러에러에러에러에러에러에러에러에러에러에러에러에러에러에러에러
코드 3-193의 테스트 케이스 실행 결과는 “failed”입니다.
실행 결과 메시지가 어떻게 출력되는지 확인해 보겠습니다.
expected: <Hello, World> but was: <Hello, JUnit>
Expected :Hello, World
Actual :Hello, JUnit
JUnit의 “failed” 메시지는 JUnit 5에 들어와서 나아지긴 했지만 JUnit 4에서는 두 번째, 세 번째 라인 조차도 출력되지 않아서 자연스러운 의미 파악이 힘든게 사실이었습니다.
✅ Hamcrest에서의 Assertion
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)));
}
}
[코드 3-194] Hemcrest 매쳐를 사용했을 때의 “failed” 메시지 예
코드 3-194에서 Hemcrest의 매쳐(Matcher)를 사용했으며, 테스트 케이스 실행 결과는 역시 “failed”입니다.
실행 결과 메시지가 어떻게 출력되는지 확인해 보겠습니다.
Expected: is "Hello, World"
but: was "Hello, JUnit"
Hemcrest의 실행 결과 “failed”에 대한 메시지 역시 하나의 문장으로 자연스럽게 읽혀지는 것을 알 수 있습니다.
이처럼 Hemcrest의 Matcher를 사용해서 사람이 읽기 편한 자연스러운 Assertion 문장을 구성할 수 있으며, 실행 결과가 “failed”일 경우 역시 자연스러운 “failed” 메시지를 확인할 수 있기때문에 가독성이 상당히 높아집니다.
JUnit → Hamcrest 예 3
그럼, 계속해서 우리가 JUnit 학습에서 사용했던 테스트 케이스를 Hamcrest로 바꿔보도록 하겠습니다.
우리가 JUnit에서 수행했던 테스트 대상 클래스는 코드 3-196의 CryptoCurrency 클래스였습니다.
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");
}
}
[코드 3-196] 테스트 대상 클래스(CryptoCurrency.java)
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에서의 Assertion
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);
}
}
‘currencyName is not Null Value.’와 같이 가독성 좋은 하나의 문장처럼 구성이 되는 것을 볼 수 있습니다.
만약 (2)를 주석 해제하면, 아래와 같은 “failed” 메시지를 확인할 수 있습니다.
Expected: is null
but: was "Ethereum"
“failed” 메시지를 하나의 문장으로 풀어보면, ‘Expected is null, but was “Ethereum”’ 같이 자연스러운 문장이 되는 것을 확인할 수 있습니다.
JUnit → Hamcrest 예 4
✅ JUnit에서의 Assertion
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();
}
}
CryptoCurrency 클래스 코드는 코드 3-196을 참고하세요.
✅ Hamcrest에서의 Assertion
public class AssertionExceptionHamcrestTest {
@DisplayName("throws NullPointerException when map.get()")
@Test
public void assertionThrowExceptionTest() {
Throwable actualException = assertThrows(NullPointerException.class,
() -> getCryptoCurrency("XRP")); // (1)
assertThat(actualException.getCause(), is(equalTo(null))); // (2)
}
private String getCryptoCurrency(String unit) {
return CryptoCurrency.map.get(unit).toUpperCase();
}
}
[코드 3-200] Hamcrest의 예외(Exception) 테스트
코드 3-200에서는 Hamcrest를 사용해서 발생한 예외가 null인지 여부를 체크하고 있습니다.
그런데, 예외에 대한 테스트는 Hamcrest 만으로 Assertion을 구성하기 힘들기 때문에
(1)과 같이 JUnit의 assertThrows() 메서드를 이용해서 assertThrows()의 리턴 값으로 전달 받은 Exception 내부의 정보를 가져와서 assertThat(expectedException.getCause(), is(equalTo(null)));처럼 추가로 검증을 진행 했습니다.
만약 Hamcrest 만으로 던져진 예외를 테스트 하기 위해서는 Custom Matcher를 직접 구현해서 사용할 수 있습니다.
코드 3-200의 테스트 케이스 실행 결과는 (1)에서 1차적으로 NullPointException이 발생하므로 (비었으니까)
(1)의 Assertion 결과는 “passed”이고,
(2)에서 결과 값인 actualException.getCause()가 null 이므로, (2)의 Assertion 결과 역시 “passed”입니다.
Hamcrest의 Custom Matcher를 구현하는 방법은 아래 [심화 학습]을 참고하세요.
핵심 포인트
- Hamcrest는 JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework이다.
- Hamcrest는 다음과 같은 이유로 JUnit에 지원하는 Assertion 메서드 보다 더 많이 사용된다.
- Assertion을 위한 매쳐(Matcher)가 자연스러운 문장으로 이어지므로 가독성이 향상 된다.
- 테스트 실패 메시지를 이해하기 쉽다.
- 다양한 Matcher를 제공한다.
- Hamcrest 만으로 던져진(thrown) 예외를 테스트 하기 위해서는 Custom Matcher를 직접 구현해서 사용할 수 있다.