3. JUnit 검증 딥 다이브
원제 : Digging Deeper into JUnit Assertions
3.1. JUnit의 검증
원제 : Assertions in JUnit
JUnit에서 검증은 테스트 내부에서 호출할 수 있는 정적 메서드를 의미한다.
해당 검증 내의 조건이 참인지 거짓인지 검증하고 결과에 따라 테스트를 성공시키거나 실패시키는 역할을 한다.
참고 테스트 도중 예외가 발생하였을 때, 이를 처리하지않으면 실패가 아닌 오류(error)로 리폿된다.
JUnit은 크게 두 가지의 검증 스타일을 제공하는데
하나는 JUnit 자체에 포함된 전통적인 스타일의 검증 스타일이고, 나머지 하나는 Hamcrest 라이브러리를 이용한 상대적으로 세련된 방식의 스타일이다.
물론 두 가지 검증 스타일을 섞어 쓰는 것도 가능은 하지만 일반적으로 하나의 스타일로 통일해서 쓰는 것이 권장된다.
3.1.1. assertTrue
원제 : assertTrue
가장 기본적인 검증은 아래와 같은 조건이 참인지 판단해보는 것이다.
아래와 같은 형태로 JUnit 5의 assertTrue
를 참조할 수 있다.
1 | import org.junit.jupiter.api.Assertions.assertTrue |
간단한 사용 예시는 아래와 같다.
1 | internal class AssertTest { |
aasertTrue()
에 파라미터로 넘기는 조건이 참인지 거짓인지 판정하는 것임을 추측할 수 있다.
테스트 코드를 수행하기 위해 필요한 정보를 좀 더 작성해보자.
1 | internal class AssertTest { |
@BeforeEach
어노테이션이 붙은 메서드를 작성하여 모든 테스트 코드의 전에 Account
객체를 생성하였다.
3.1.2. assertThat을 통한 명확한 값의 비교
원제 : assertThat Something Is Equal to Another Something
JUnit을 통한 단위 테스트에서 대부분의 검증은 기대하는 값 그리고 실제로 반환된 값을 비교한다.
예를 들어 특정 잔고가 “0보다 크다” 라고 비교하기 보다는 “100이 아니다” 라고 명시적으로 비교하는 것이 더 선호된다.
이때 hamcrest 라이브러리의 assertThat()
메서드를 사용하면 아래와 같이 작성할 수 있다.
1 | assertThat(account.balance(), equalTo(100)) |
assertThat()
메서드는 첫 번째 매개변수로 실제 표현식(actual) 을, 두 번째 매개변수로 매처(matcher) 를 넘겨받아 두 매개변수의 결과를 비교한다.
이렇게 작성하는 방식은 테스트 코드를 실제 술어처럼 읽을 수 있기때문에 높은 가독성을 표현하는 데 도움을 준다.
아래 assertThat()
메서드의 구현부를 보면 이 의도를 좀 더 자세히 알 수 있다.
1 | public static <T> void assertThat(T actual, Matcher<? super T> matcher) { |
언뜻 보기에 assertTrue()
와 assertThat()
의 차이는 거의 없어보인다.
하지만 실제 테스트를 수행해서 실패하는 경우 단순히 실패로 처리하느냐, 좀 더 나은 정보를 출력해주느냐의 차이가 있다.
만약 계좌의 잔고가 101인 경우에 아래와 같이 테스트한다고 가정해보자.
1 | assertTrue(account.balance() == 100) |
101과 100은 다르기 때문에 테스트는 실패하며 아래와 같은 메시지를 출력한다.
1 | expected: <true> but was: <false> |
이번엔 assertThat()
을 사용해보자.
1 | assertThat(account.balance(), equalTo(100)) |
이 테스트도 실패하며 아래와 같은 메시지를 출력한다.
1 | Expected: <100> |
3.1.3. 주요 Hamcrest Matcher 살펴보기
원제 : Rounding Out the Important Hamcrest Matchers
Hamcrest 라이브러리내 CoreMatcher 클래스는 바로 매처 작업을 할 수 있도록 도구를 제공해준다.
제공해주는 모든 메서드는 아래 링크를 참고하면 된다.
배열 검증
1 | assertThat(arrayOf("a", "b", "c"), equalTo(arrayOf("a", "b"))) // fail |
컬렉션 검증
is
1 | assertThat(account.name, `is`(equalTo("an account name"))) |
not
1 | assertThat(account.name, `is`(not(equalTo("an different account name")))) |
이렇듯 JUnit에 Hamcrest 라이브러리를 사용하는 경우 아래와 같은 방식의 테스트를 진행할 수 있다.
- 객체의 타입을 검증한다.
- 두 객체의 참조가 같은 인스턴스인지 검증한다.
- 다수의 매처를 결합하여 둘 다 혹은 둘 중에 어던 것이든 성공하는 지 검증한다.
- 어떤 컬렉션이 특정 요소를 포함하거나 조건에 부합하는 지 검증한다.
- 어떤 컬렉션이 아이템 몇 개를 모두 포함하는 지 검증한다.
- 어떤 컬렉션에 있는 모든 요소가 매처를 준수하는 지 검증한다.
3.1.4. 부동소수점 비교 검증
원제 : Comparing Two Floating-Point Numbers
컴퓨터에서 부동소수점은 정확한 값이 아닌 근사치로 표현된다.
근사치로 표현하는 경우 테스트의 때로는 성공하고 때로는 실패하는 등의 결과로 이어질 수 있다.
아래와 같은 테스트 코드가 있다고 가정해보자.
1 | assertThat(2.32 * 3, equalTo(6.96)) |
2.32 * 3은 6.96이므로 테스트는 성공할 것이라고 기대된다.
하지만 실제로 테스트를 수행하면 아래와 같은 메시지를 출력하며 실패하게 된다.
1 | Expected: <6.96> |
즉 우리가 검증하려는 대상이 Float
혹은 Double
로 표현되는 부동소수점인 경우 두 부동소수점 사이의 공차나 허용 오차를 지정해야한다.
이를 위해 테스트 코드를 아래와 같이 바꿔보자.
1 | assertTrue((2.32 * 3 - 6.96).absoluteValue < 0.0005) |
이제 테스트는 성공한다.
다만 우리는 가독성을 잃어버리게 되었다.
잃어버린 가독성을 되찾기위해 Hamcrest를 활용해보자.
1 | assertThat(2.32 * 3, closeTo(6.96, 0.0005)) |
위와 같이 closeTo()
매처를 사용하면 가독성도 확보할 수 있다.
3.1.5. 검증에 대한 명세
원제 : Explaining Asserts
위에서 언급한 assertThat()
에 대한 구현체를 다시 살펴보자.
1 | public static <T> void assertThat(T actual, Matcher<? super T> matcher) { |
모든 assertThat()
은 결국 assertThat(String reason, T actual, Matcher<? super T> matcher)
을 호출하고 있는데
여기서 첫 번째 매개변수인 reason
을 확인할 수 있다.
reason
은 검증의 근거를 설명해주는 역할을 수행한다.
1 |
|
위와 같이 reason
을 추가하면 검증의 근거를 확인할 수도 있다.
다만 테스트 메서드의 이름만 보고도 알 수 있게 작성하는 것이 더욱 좋은 방향이다.
3.2. 예외를 예상하는 세 가지 방법
원제 : Three Schools for Expecting Exceptions
모든 코드가 해피 케이스만 보장되면 좋겠지만, 예외를 던지는 경우도 많다.
예를 들어 클라이언트가 잔고보다 많은 돈을 인출하려는 경우에 예외를 던지는 것을 생각해볼 수 있다.
이때 JUnit을 이용하면 세 가지 방법으로 예외를 던지는 것을 명시할 수 있다.
3.2.1. 어노테이션을 활용하는 방법
원제 : Simple School: Using an Annotation
어노테이션으로 예외 발생을 기대하게할 수도 있다.
아래 예제를 보자.
1 |
|
위 테스트 코드에서 InsufficientFundsException
예외가 발생하는 경우에는 기대된 동작이므로 테스트는 성공한다.
3.2.2. try-catch를 활용하는 방법
원제 : Old School: Try and Fail-or-Catch
전통적인 예외처리 방식인 try-catch를 활용하는 것도 방법이다.
1 |
|
만약 예외가 발생하는 경우 상태를 검증하려는 경우 아래와 같이 처리하는 것도 방법이다.
1 |
|
3.2.3. ExpectedException을 활용하는 방법
원제 : New School: ExpectedException Rules
JUnit은 커스텀 규칙을 정의하여 테스트 실행 도중 발생하는 일에 대한 통제권을 강화할 수 있다.
특히 ExpectedException을 활용하면 예외를 검사하는 데 편리하다.
이제 새롭게 만들어 잔고가 0인 계좌에서 돈을 인출하는 테스트를 설계해보자.
1 |
|
thrown
규칙 인스턴스는 InsufficientFundsException
예외가 발생함을 알려주는 규칙이 된다.
3.2.4. 예외 무시
원제 : Exceptions Schmexceptions
개발자가 작성하는 테스트는 대부분 해피 패스인 경우가 많다.
하지만 검증된 예외를 처리할 수 있도록 테스트 코드를 작성하여 좀 더 안정적인 애플리케이션을 개발하는 것이 중요하다.
결론적으로 검증된 예외를 처리하려고 try-catch
블록을 수행하는 것보다 다른 예외를 던지도록 하여 검증하는 것이 좋다.
아래 예제를 보자.
1 |
|
위와 같은 테스트 코드처럼 예외를 처리하지않고 발생하게 하는 것이 오히려 테스트 코드를 통한 애플리케이션의 강건성 확보에 더욱 도움이 될 것이다.