TDD와 JUnit, AssertJ

크리링·2023년 2월 25일
0
post-thumbnail

문제 해결 방법으로 TDD와 DDD를 자주 보았다. 그 중 TDD는 어떤 특징을 가지고 있으며, 현재 내가 쓰는 방식과의 차이와 개선점을 찾아보려고 한다.






TDD

TDD (Test Driven Development) : 테스트 주도 개발
테스트 주도 개발은 매우 짧은 개발 사이클을 반복하는 소프트웨어 개발 프로세스 중 하나이다. 개발자는 먼저 요구사항을 검증하는 자동화된 테스트 케이스를 작성한다. 그런 후에, 그 테스트 케이스를 통과하기 위한 최소한의 코드를 생성한다. 마지막으로 작성한 코드를 표준에 맞도록 리팩토링한다. 이 기법을 개발했거나 '재발견'한 것으로 인정되는 Kent Beck은 2003년에 TDD가 단순한 설계를 장려하고 자신감을 불어넣어준다고 말하였다.



TDD의 특징

TDD는 매우 짧은 개발 서클의 반복에 의존하는 소프트웨어 개발 프로세스이다.

우선 개발자는 요구되는 새로운 기능에 대한 자동화된 테스트 케이스를 작성하고, 해당 테스트를 통과하는 짧고 가독성이 좋고 유지보수성이 뛰어난 코드를 작성한다. 일단 테스트 통과하는 코드를 작성하고 상황에 맞게 리팩토링하는 과정을 거치는 것이다.



메인 함수에서 하는 테스트의 문제점

  1. 프로덕션 코드와 테스트 코드가 하나에 존재한다.

  2. 테스트 코드가 실제 서비스에 같이 배포된다.

  3. 메인 메소드 하나에서 여러 개의 기능을 테스트하기 때문에 복잡도가 증가한다.

  4. 메소드 이름을 통해서 어떤 부분을 테스트 하는것인지 알기가 힘들다.

  5. 테스트 결과를 사람이 수동으로 확인해야한다. (내가 사용하는 테스트 방법의 가장 큰 문제)


main method를 사용한 테스트를 해결하기 위해 등장한게 바로 JUnit이다.






JUnit

JUnit은 단위 테스트 프레임워크로, System.out으로 번거롭게 테스트 케이스를 확인하지 않도록 도와주는 도구이다. 프로그램 테스트 시 걸리는 시간도 관리할 수 있도록 해주며, 오픈소스 형태로 대부분의 IDE에 포함되어있다. 개발을 진행하면서 어느정도의 개발이 진행되면 반드시 프로그램에 대한 단위 테스트를 진행해주어야 한다.



  • @Test : 메소드 위에 해당 어노테이션을 선언해, 테스트 대상 메소드 임을 지정할 수 있다.

  • @Test (timeout=밀리초) :
    테스트 메소드 수행 시간을 제한할 수 있다. 테스트 메소드가 리턴 값을 반환하는 데에 걸리는 시간이 지정된 밀리초를 넘긴다면 해당 테스트는 실패로 판별한다.

  • @Test(expected=예외) : 해당 테스트 메소드 예외 발생 여부에 따라 성공/실패를 판별할 수 있다. expected=에 지정된 예외가 발생해야 테스트가 성공한 것으로 판별한다.

  • @Ignore : 해당 어노테이션이 선언된 테스트 메소드를 실행하지 않도록 지정한다.

  • @BeforeEach : 모든 @Test 메소드가 실행되기 전에 실행되는 메소드를 지정하는 어노테이션이다.

    • 각 테스트 시작 전에 각각 호출된다.
    • @Test 메소드에서 공통으로 사용되는 코드를 @BeforeEach 메소드에 선언해 사용하면 좋다.
    • 테스트마다 공통으로 쓰이면서, 테스트 전에 리셋되어야 할 항목이 들어간다.
  • @AfterEach : 모든 @Test 메소드의 실행이 끝난 뒤에 실행되는 메소드를 지정하는 어노테이션이다.

    • 각 테스트가 끝나고 각각 호출된다.
  • @BeforeAll : 해당 테스트 클래스가 실행될 때 딱 한번만 수행되는 테스트 메소드를 지정하는 어노테이션이다.

  • @AfterAll : 해당 테스트 클래스가 실행이 끝난 뒤에 딱 한번만 수행되는 테스트 메소드를 지정하는 어노테이션이다.

    • 테스트 클래스의 모든 테스트가 완료된 뒤 한번 호출된다.






AssertJ

AssertJJUnit과 마찬가지로 테스트를 위한 라이브러리로, JUnit과 함께 사용하면 테스트 코드를 훨씬 가독성 있고 효율적으로 작성할 수 있도록 도와준다.



(org.assertj.core.api 패키지)

Assertion 객체가 기본적으로 제공하는 인터페이스

  • isEqualTo(Object o), isNotEqualTo(Object o) : 실제 값이 주어진 값과 같은지/다른지 확인

  • isInstanceOf(Class<?> type), isInstanceOfAny(Class<?> ... types), isNotInstanceOf(Class<?> type), isNotInstanceOfAny(Class<?> ... types) : 실제 값이 주어진 유형의 인스턴스인지 아닌지 확인

  • isExactlySameInstanceOf(Class<?> type), isNotExactlyInstanceOf(Class<?> type) : 실제 값이 정확히 주어진 유형의 인스턴스인지 확인

  • asList() : 실제 값이 List의 인스턴스인지 확인하고 list Assertion을 반환

  • asString() : toString()으로 실제 값에 대한 문자열을 반환

  • hasSameClassAs(Object o), doesNotHaveSameClassAs(Object o), hasSameHashCodeAs(Object o), doesNotHaveSameHashCodeAs(Object o) : 실제 값 / 객체가 주어진 객체와 동일한 클래스 / 해시코드를 가지고 있는지 확인 doesNot이 붙은 메소드는 반대로, 가지고 있지 않은지를 확인

  • hasToString(String str), doesNotHaveToString(String str) : 실제 actual.toString() 값이 주어진 String과 같은지 확인 doesNot이 붙은 메소드는 반대로, 같지 않은지 확인

  • isIn(Iterable<?> v), isIn(Object ... v), isNotIn(Iterable<?> v), isNotIn(Object ... v) : 주어진 iterable 또는 값 배열에 실제 값이 있는지/없는지 확인

  • isNull(), isNotNull() : 실제 값이 null인지 확인

  • isSameAs(Object o), isNotSameAs(Object o) : == 비교 사용해 실제 값이 주어진 값과 동일한지 아닌지 확인

  • as("설명") : Àssertion을 설명하는 메소드. "설명"의 내용이 테스트 결과에 출력되도록 할 수 있다.



숫자 관련 메소드

  • isBetween(start, end) : 실제 값이 start에서 end 값 사이에 있는지 확인함.

  • isStrictlyBetween(start, end) : 실제 값이 start에서 end 값 사이에 있는지 확인함.

  • isCloseTo(expected, offset), isCloseTo(expected, percentage) : 실제 숫자가 주어진 offset / percentage 내에서 expected에 가까운지 확인. 차이가 offset/percentage와 같으면 Assertion은 유효한 것으로 간주.

  • isNotCloseTo(expected, offset), isNotCloseTo(expected, percentage) : 실제 숫자가 주어진 offset/percentage 내에서 expected에 가깝지 않은지 확인. 차이가 offset/percentage와 같으면 Assertion은 실패한 것으로 간주

  • isPositive(), isNegative() : 실제 값이 양수인지/음수인지 확인함

  • isNotPositive(), isNotNegative() : 실제 값이 양수가 아닌지 (음수이거나 0인지) / 음수가 아닌지 (양수이거나 0인지) 확인함

  • isZero(), isNotZero() : 실제 값이 0인지 아닌지 확인

  • isOne() : 실제 값이 1과 같은지 확인



contains

contains

void containsTest() {
    List<Integer> list = Arrays.asList(1, 2, 3);

    // Success: 모든 원소를 입력하지 않아도 성공
    assertThat(list).contains(1, 2);

    // Success: 중복된 값이 있어도 포함만 되어 있으면 성공
    assertThat(list).contains(1, 2, 2);

    // Success: 순서가 바뀌어도 값만 맞으면 성공
    assertThat(list).contains(3, 2);

    // Fail: List 에 없는 값을 입력하면 실패
    assertThat(list).contains(1, 2, 3, 4);
}

String, array, Array, Set에도 사용 가능

@Test
void stringContainsTest() {
    String str = "abc";
    assertThat(str).contains("a", "b", "c");
}

@Test
void arrayContainsTest() {
    int[] arr = {1, 2, 3, 4};
    assertThat(arr).contains(1, 2, 3, 4);
}

@Test
void setContainsTest() {
    Set<Integer> set = Set.of(1, 2, 3);
    assertThat(set).contains(1, 2, 3);
}

containsOnly : 순서, 중복을 무시하는 대신 원소값과 갯수가 정확히 일치
containsExactly : 순서를 포함해서 정확히 일치

@Test
void containsOnlyTest() {
    List<Integer> list = Arrays.asList(1, 2, 3);

    assertThat(list).containsOnly(1, 2, 3);
    assertThat(list).containsOnly(3, 2, 1);
    assertThat(list).containsOnly(1, 2, 3, 3);
}


@Test
void containsExactlyTest() {
    List<Integer> list = Arrays.asList(1, 2, 3);

    assertThat(list).containsExactly(1, 2, 3);
}



AssertJ 사용시 주의점

  • AssertThat(Object o)로 테스트할 객체를 호출한 다음 메소드들을 사용해야 한다.

  • as()는 assertion 메소드들을 호출하기 전에 사용해야 한다.






그동안 내가 쓴 방식은 도메인을 먼저 만들고 서비스 코드를 만들기 전에 테스트 JUnit을 사용해서 System.out으로 값을 확인하는 과정을 거치고 참고해서 서비스 코드를 만드는 편이었다. 하지만 이 방법에서 값을 일일이 확인하는 과정을 AssertJ를 통해 덜을 수 있다는게 올바른 테스트 코드 작성 방법이라고 생각해 JUnit과 Assertion을 적극 활용하도록 해봐야겠다.






출처 및 참고 :
TDD로 개발하기 2탄,

[Java] 플레이그라운드 with TDD, 클린코드 - AssertJ
,
테스트 주도 개발 - 위키피디아,
[Java] Junit, AssertJ의 개념 및 기초적인 사용법(단정문, 어노테이션)

0개의 댓글