Java

[Java] JUnit의 assertThat보다 assertj의 assertThat을 써야하는 이유

jwKim96 2021. 10. 13. 18:21

이 글은 JUnit4를 기준으로 작성되었습니다.


테스트 코드를 작성할 때, 크게 두 부분으로 나누어서 생각할 수 있습니다.

1. 실행 단계
2. 검증 단계

실행 단계에서는 사용자 입력을 모방하여 테스트할 로직을 실행시킵니다.
검증 단계에서는 실행 단계가 예상한 대로 동작하는지 검증하게 됩니다.

JUnit에서 이 검증 단계를 도와주는 메소드가 바로 org.junit.Assert.assertThat이라는 메소드 입니다.

org.junit.Assert.assertThat

공식 문서

org.junit.Assert.assertThat의 기본 형태는 아래와 같습니다.

public static <T> void assertThat(T actual, Matcher<? super T> matcher)

actual 인자에 검증대상(실행 단계의 결과)을 넣고, 이와 비교하는 로직(matcher)을 주입받아 검증 단계를 수행합니다.

개발자가 matcher를 직접 구현하는것은 비효율적이고, 구현한 matcher에서 오류가 발생할 수도 있습니다.
matcher에서 오류가 발생하면 테스트가 실패하는데, 외부 요인에 영향을 받는것은 좋은 테스트라고 할 수 없습니다.
그런 이유 때문인지, JUnit의 assertThat은 hamcrest에 구현된 matcher를 사용하도록 강제하고 있습니다.


서비스의 제약조건이 [ 0보다 커야한다 ] 라고 가정하고, JUnit의 assertThat을 이용한 테스트 코드 예제를 살펴보겠습니다.

예제 코드(JUnit)

import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.greaterThan;

    ...

    @Test
    public void number_test() throws Exception {

        int number = 3;

        int result = mathService.add(number, 3)

        assertThat(result, greaterThan(0));

    }

mathService를 통해서 number와 3을 더하는 서비스를 실행했습니다.

그리고 아래에서는 assertThat에서 검증대상과 matcher를 인자로 받아 제약조건을 만족하는지 검증합니다.

이렇게 hamcrest를 함께 사용하게되면 손쉽게 원하는 테스트 로직을 수행할 수 있고, 직관적인 테스트 코드가 완성됩니다.

하지만, 제약조건이 [ 0보다 크고 10보다는 작아야 한다 ] 라고 변경된다면 테스트 코드를 아래와 같이 수정해야합니다.

import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.core.AllOf.allOf;

    ...

    @Test
    public void number_test() throws Exception {

        int number = 3;

        int result = mathService.add(number, 3)

        assertThat(result, allOf(
            greaterThan(0),
            lessThan(10)
        ));
    }

원하는 테스트 로직은 완성했는데, 제가 이 예제 코드를 작성하며 불편한 점이 있었습니다.

1. 자동완성
allOf, greaterThan, lessThan 등의 메소드들을 미리 import를 해놓지 않으면,
IDE에서 자동완성을 해주지 못하기 때문에 필요한 메소드를 공식문서에서 찾거나, 이름을 외워서 작성해야 했습니다.

2. Assertion 분류(matcher)
위 로직에서는 int 타입에 대한 matcher만 필요합니다.
하지만 org.hamcrest.Matchers클래스에는 여러 타입에 대한 matcher가 함께 들어있습니다.
그래서 원하는 matcher를 찾기가 불편했습니다.

3. 확장성
추가된 조건도 같이 검증하기 위해서 allOf이라는 메소드로 기존 조건과 묶어줘야 했습니다.

(개인적으로는 가독성도 그렇게 좋지는 않다고 생각합니다)

assertj에서는 이런 불편함을 해결하고 메소드 체이닝 패턴 형식으로 사용 가독성을 향상시킨 assertThat을 제공하는데요.

이 글의 주인공인 org.assertj.core.api.Assertions.assertThat를 아래에서 살펴봅시다.

org.assertj.core.api.Assertions.assertThat

공식 문서

public static AbstractAssert<SELF, T> assertThat(T actual)

assertj의 assertThat은 인자로 actual(검증대상)만 받습니다.

그리고 actual의 타입에 따라 AbstractAssert라는 추상클래스를 상속한 Assert 클래스를 반환합니다.

아래에서 몇개만 살펴봅시다.

public static AbstractIntegerAssert<?> assertThat(int actual)
public static AbstractDateAssert<?> assertThat(Date actual)
public static AbstractUriAssert<?> assertThat(URI actual)

타입에 따라 AbstractIntegerAssert, AbstractDateAssert, AbstractUriAssert 등의 Assert 클래스를 반환하고

각 Assert 클래스는 타입에 맞는 Assertion 메소드를 제공합니다.

- AbstractIntegerAssert 클래스 (java.lang.Integer)
isGreaterThan(int other)
isLessThan(int other)
...

-AbstractDateAssert 클래스 (java.util.Date)
hasTime(long timestamp)
hasYear(int year)
isAfter(Date other)
isBefore(Date other)
...

-AbstractUriAssert 클래스 (java.net.URI)
hasNoParameters()
hasNoQuery()
hasNoPort()
...

각 Assertion 메소드는 반환 타입이 SELF 이기 때문에 메소드 체이닝 패턴으로 테스트 로직을 작성할 수 있습니다.

예를 들어, AbstractIntegerAssert.isGreaterThan(0) 메소드는 AbstractIntegerAssert를 다시 반환합니다.

그러면 이어서 AbstractIntegerAssert.isGreaterThan(0).isLessThan(10) 형태로 작성할 수 있는데요.

그래서 아래와 같이 테스트 코드를 구현할 수 있습니다.

예제 코드(assertj)

import static org.assertj.core.api.Assertions.assertThat;

    ...

    @Test
    public void number_test() throws Exception {

        int number = 3;

        int result = mathService.add(number, 3)

        assertThat(result)
                .isGreaterThan(0)
                .isLessThan(10);

    }

마치 자연어 처럼 읽히고, [ 0보다 크고 10보다는 작아야 한다 ] 라는 제약조건을 한눈에 알아볼 수 있습니다.

assertj의 assertThat을 사용하니, JUnit의 assertThat과 비교하여 다음과 같은 장점이 느껴졌습니다.

1. 자동완성
assertThat에서 반환되는 Assert 클래스를 사용하기 때문에, 메소드 자동완성이 지원되어 편리합니다.

2. Assertion 분류
assertThat에서 인자의 타입에 맞는 Assert 클래스를 반환하기 때문에, 필요한 메소드만 분류되어있습니다.

3. 확장성
체이닝 메소드 패턴으로 작성 가능하기 때문에, 조건 추가를 위해 추가 작업이 필요없어 편리하고 가독성도 좋습니다.

정리

이 글에서 두 라이브러리(JUnit, AssertJ)가 제공하는 assertThat이라는 메소드를 비교해봤습니다.

정리하면, AssertJ의 assertThat을 사용해야하는 이유는 자동완성, Assertion 분류, 확장성 입니다.

필요한 메소드를 검색하고 알맞은 항목을 찾아 import 해야하는 번거로움이 없을 뿐더러, 가독성도 좋습니다.

가독성 좋은 코드는 유지보수와 리펙토링이 용이하기 때문에 생산성을 향상시키는 좋은 효과를 불러옵니다.


긴 글 읽어주셔서 감사합니다.

오탈자 혹은 제가 잘못 알고있는 내용이 있다면, 댓글 남겨주시면 감사하겠습니다.