티스토리 뷰

자바/테스트

[JUnit] AssertJ Assertions

다음김 2023. 6. 23. 17:33

 

지난 글에서는 JUnit이 제공하는 Assertions에 대해 알아보았다. 이번에 알아볼 AssertJ의 Assertions는 JUnit의 Assertions 보다 더 유연하고 가독성 있는 테스트 코드를 작성할 수 있게 해주며, 특히 자바 8의 함수형 프로그래밍의 특징들을 잘 활용할 수 있게 해준다. 그럼 이 AssertJ가 뭔지 간단히 살펴보고 AssetJ의 Assertions 메소드들을 자주 사용하는 것들 위주로 정리해보자.

 

  JUnit
 자바 기반 테스트 자동화 프레임워크

  AssertJ
 자바 기반 테스트에서 Assertions를 작성하기 위한 오픈소스 라이브러리

 

 

즉 JUnit에 비해 AssertJ가 Assertion에 더 특화되어 있다고 보면 된다. 자바에서 AssertJ를 사용하기 위해서는 build.gradle의 dependencies에 다음 코드를 추가해주어야 하지만, 스프링 프로젝트의 경우 별도로 지정하지 않아도 AssertJ 라이브러리가 자동으로 포함된다.

testImplementation 'org.assertj:assertj-core:3.23.1'

 

 

JUnit만으로도 충분한데 왜 굳이 AssertJ를 사용해야 하는지에 대한 의문은 아래 코드로 해결될 것이다. 한 눈에 보기에도 AssertJ 코드가 훨씬 더 깔끔하다. 또한 메소드 체이닝으로 검증 메소드들을 순차적으로 호출함으로써 여러 개의 검증을 마치 하나의 문장처럼 표현할 수 있어 가독성이 향상된다.

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

class UnitTest {

    @Test
    void testByAssertJ() {
        assertThat("hello world!")
                .hasSize(12)
                .contains("hello", "world")
                .endsWith("!");
    }

    @Test
    void testByJUnit() {
        String str = "hello world!";
        assertEquals(12, str.length());
        assertTrue(str.contains("hello"));
        assertTrue(str.contains("world"));
        assertTrue(str.endsWith("!"));
    }
}

 

 

 

AssertJ Assertions로 검증하기

assertThat(new Object()); //Assertions 클래스 static import

JUnit의 Assertions에서는 검증 대상의 타입에 따라 각기 다른 메소드를 호출했던 것과는 다르게, AssertJ에서는 대부분 위와 같은 Assertions.assertThat(...) 메소드를 시작으로 테스트 코드가 작성된다. 이때 assertThat 메소드의 인자로 검증하고자 하는 실제 객체를 넘겨주며, assertThat 메소드 뒤에 연결되는 메소드들이 해당 객체에 대한 검증을 수행한다. 

 

assertThat 메소드는 전달되는 인자에 따라 '(Abstract) + (검증 대상의 타입) + Assert' 이름을 가진 클래스의 객체(이하 Assert 객체)를 생성하여 반환하고, 이후 반환된 Assert 객체의 메소드들을 체이닝하여 검증하는 것이다. 

 

 

new String("abc").concat("efg").concat("hij");

메소드 체이닝은 위와 같이 메소드가 마치 체인처럼 연속적으로 호출되는 것을 말한다. 이는 concat 메소드가 다시 String 객체를 반환하기 때문에 가능한 것으로, Assert 객체의 검증 메소드 또한 Assert 객체(즉 자기 자신, SELF)를 반환하기 때문에 method chaining이 가능한 것이다.

 

 

전달된 인자에 따라 assertThat 메소드가 반환하는 Assert 객체

*참고로 Assert가 상위 인터페이스이며 ObjectAssert, AbstractBooleanAssert 등은 구현 클래스

 

 

 

Object Assertions 

class UnitTest {
    static class A {
        int a;

        public A(int a) {
            this.a = a;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof A) return ((A) obj).a == a;
            return false;
        }
    }

    @Test
    void test() {
        A a1 = new A(1), a2 = new A(1);
        assertThat(a1)
                .as(() -> "Not Equal!").isEqualTo(a2)
                .as(() -> "Not Same!").isSameAs(a2);
    }
}

객체의 동등성을 비교하기 위해 JUnit에서는 assertEquals(expected, actual) 메소드를 호출하였다. AssertJ에서도 isEqualsTo 메소드를 통해 동등성 비교를 수행할 수 있는데, 이때 올바른 비교를 위해서는 비교 대상 클래스의 equals 메소드가 오버라이딩 되어 있어야 한다. 동일성 비교 또한 비슷하게 isSameAs 메소드를 통해 수행할 수 있다.

 

 

JUnit에서는 검증 메소드 인자로 넘겨주었던 테스트 실패 메시지도 AssertJ에서는 as 라는 별도의 메소드로 분리하여 전달할 수 있다. 이때 주의해야 할 것은 특정 assertion 메소드에 대한 테스트 실패 메시지를 지정할 때 as 메소드는 해당 assertion 메소드 보다 앞서서 호출되어야 한다는 것이다. 즉 isEqualTo(...).as(...)와 같이 메소드의 호출 순서를 바꾸면 isEqualTo assertion이 실패해도 as 메소드에서 지정한 테스트 실패 메시지가 콘솔창에 뜨지 않는다. 그 이유는 chaining된 assertions가 순서대로 호출되는 동안 하나라도 테스트에 실패하면 AssertionError를 throw하여, 뒤에 남아있는 메소드들이 더이상 호출되지 않고 실행이 중단되기 때문이다.

 

 

 

Boolean Assertions

@Test
void test() {
    BooleanSupplier condition = () -> 1 == 2;
    assertThat(1 == 1).isTrue();
    assertThat(1 == 2).isFalse();
    //assertThat(condition).isFalse(); //오류 발생!
}

JUnit의 assertTrue, assertFalse와 같이 Boolean 값을 검증하기 위한 메소드로 isTrue(), isFalse() 메소드를 제공한다. 다만 JUnit과 다른 점은 BooleanSupplier 객체를 assertThat 메소드의 인자로 전달하고 이후 isTrue(), isFalse() 메소드를 호출할 순 없다.

 

 

 

Iterable/Array Assertions

@Test
void test() {
    List<Integer> list = List.of(1, 2, 3, 4, 5);
    assertThat(list).isNotEmpty()
            .hasSize(5)
            .doesNotContainNull()
            .containsSequence(1, 2, 3);
}

다수의 elements를 가진 Iterable 또는 Array 타입의 객체에 대해서는 해당 객체가 비어있는지, 요소의 갯수가 몇 개인지, 특정 요소를 포함하고 있는지 등을 검증할 수 있다. JUnit에서는 해당 Iterable/Array 객체의 메소드를 직접 호출하고 반환값을 assertion 메소드의 인자로 전달하는 방식으로 검증하였는데, AssertJ에서는 Iterable/Array 객체의 메소드를 직접 호출할 필요 없이 해당 객체를 검증할 수 있다.

 

예를 들어 특정 Iterable/Array가 비어있지 않으면서 요소의 개수가 5개이고, null인 요소를 포함하지 않으면서 (1, 2, 3)의 순열을 포함하는지를 검증하는 테스트 코드는 위와 같이 작성할 수 있다. 반면 JUnit에서는 직접 list 객체의 size() 메소드를 호출하고 그 반환값을 assertion 메소드로 검증하는 등 번거로운 작업이 필요하다.

 

 

 

Character Assertions

@Test
void test() {
    assertThat('a')
            .isGreaterThan('A')
            .isLessThanOrEqualTo('a')
            .isLowerCase();
}

문자 또한 문자 객체에 대한 메소드 호출 없이 다양한 검증을 수행할 수 있다. 예를 들어 위와 같이 어떤 문자가 특정 문자보다 큰지 또는 작거나 같은지, 소문자인지 등을 검증할 수 있다.

 

 

 

Class Assertions

@Test
void test() {
    assertThat(List.class).isInterface();
    assertThat(Throwable.class).isAssignableFrom(Exception.class);
    assertThat(new Exception()).isInstanceOf(Exception.class);
}

assertThat 메소드 인자로 클래스 타입을 전달하여 해당 클래스 타입이 인터페이스인지 또는 다른 클래스의 부모 클래스인지 또한 검증할 수 있다. 뿐만 아니라 isInstanceOf assertion으로 검증 대상 객체가 주어진 클래스의 객체인지도 확인할 수 있다.

 

 

 

Map Assertions 

@Test
void test() {
    Map<Integer, Character> map = new HashMap<>();
    map.put(1, 'a'); map.put(2, 'b'); map.put(3, 'c');
    assertThat(map).isNotEmpty()
            .containsKey(1)
            .containsValue('c')
            .contains(entry(2, 'b'));
}

Iterable/Array의 Assertions와 비슷하게 Map 또한 비어있는지, 특정 key-value 쌍(entry)을 포함하고 있는지, 특정 key를 포함하는지 또는 특정 value를 포함하는지 각각을 손쉽게 검증할 수 있다.

 

 

 

Throwable Assertions

@Test
void test() {
    assertThatThrownBy(() -> {
        throw new IllegalArgumentException("Throwing a IllegalArgumentException");
    }) //AbstractThrowableAssert 객체 반환
    .isInstanceOf(Exception.class).hasMessage("Throwing a IllegalArgumentException");
}

JUnit의 assertThrow와 같이 특정 코드 실행 시 특정 타입의 예외가 발생하는지를 assertThatThrownBy 메소드를 통해 검증할 수 있다. 이 또한 AbstractThrowableAssert 객체를 반환하여 해당 코드에서 throw된 예외에 대한 message, cause, stacktrace 등도 추가로 검증 가능하다.

 

 

 

Collection Filtering

//JUnit
@Test
void test() {
    String str1 = "Java", str2 = "Spring", str3 = "Spring Boot";
    List<String> list = List.of(str1, str2, str3);
    List<String> actual = list.stream()
            .filter(s -> s.contains("Spring"))
            .toList();
    List<String> expected = List.of(str2, str3);
    assertIterableEquals(expected, actual);
}

//AssertJ
@Test
void test() {
    String str1 = "Java", str2 = "Spring", str3 = "Spring Boot";
    List<String> list = List.of(str1, str2, str3);
    assertThat(list) //AbstractIterableAssert 객체 반환
            .filteredOn(s -> s.contains("Spring"))
            .contains(str2, str3);
}

JUnit에서는 검증 대상인 collection의 필터링을 assertion 외부에서 수행한 후 필터링 결과를 변수에 저장하고 이를 assertion 메소드에 전달해줘야 하는데 반해, AssertJ에서는 이 collection 필터링도 대신 수행해주는 assertion 메소드가 존재한다. (실제 AbstractIterableAssert 객체의 filteredOn 메소드 내부에서 Streamfilter 메소드를 호출한다.) 따라서 collection 필터링 후 검증 메소드들을 체이닝할 수 있어 코드가 더 짧아지고 가독성도 향상된다.

 

 

 

 

확실히 JUnit의 Assertions에 비해 AssertJ의 Assertions가 메소드 개수도 더 많고 코드의 가독성도 더 좋다. 또한 IDE의 자동 완성 기능으로 AssertJ의 모든 메소드를 외우지 않더라도 상황에 맞게 적절한 assertion을 골라 사용할 수 있다. 이러한 이유로 실무에서는 단위 테스트에 AssertJ를 더 많이 사용하지 않을까 싶다. 오늘 정리한 내용은 AssertJ의 자주 사용되면서도 기본적인 메소드들로, 더 자세히 공부하고 싶다면 다음을 참고하길 추천한다.

 

AssertJ's Java 8 Features

Custom Assertions with AssertJ

 

IDE의 AssertJ 메소드 자동 완성 기능

 

 

 

끝.

 

 

 

참고

https://www.baeldung.com/introduction-to-assertj

https://annaduldiier.medium.com/assertj-vs-junit-483b7d6dc997

 

'자바 > 테스트' 카테고리의 다른 글

[Mockito] Mocking Framework with JUnit5  (1) 2023.10.07
[JUnit] Parameterized Test  (0) 2023.06.24
[JUnit] JUnit5 Assertions  (0) 2023.06.20
[JUnit] 테스트 Tagging과 Filtering  (0) 2023.06.02
[JUnit] 자바 단위 테스트 프레임워크  (0) 2023.05.31
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함