Study/TDD

TDD - JUnit 5 기초

hou27 2023. 1. 25. 22:03

이전 포스트

https://hou27.tistory.com/entry/TDD-TDD-%E2%88%99-%EA%B8%B0%EB%8A%A5-%EB%AA%85%EC%84%B8-%E2%88%99-%EC%84%A4%EA%B3%84

 

TDD - TDD ∙ 기능 명세 ∙ 설계

이전 포스트 https://hou27.tistory.com/entry/TDD-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1-%EC%88%9C%EC%84%9C TDD - 테스트 코드 작성 순서 이전 포스트 https://hou27.tistory.com/entry/TDD-TDD%EB%9E%80 TDD - TDD(Test-

hou27.tistory.com

 

실습 환경

- Java 17

- Spring Boot 3.0.1

- wsl 2

- Gradle

- JUnit 5 ver 5.9.1

 

지금까지 @Test 애노테이션과 assertEquals() 메서드만 사용해서 테스트 코드를 작성했었다.

JUnit을 잘 활용하기 위해선 추가적으로 여러 가지를 알아야 한다.

 

책은 JUnit 5.5 버전을 대상으로 설명하고 있었다.

 

사용하던 버전은

JUnit 5.9.1 버전이므로 책을 참고하면서 버전에 맞게 정리하도록 하겠다.

(2023.01.10 5.9.2 버전이 릴리즈 되긴 했다.)

 

JUnit 5 모듈 구성

이전 버전이 단일 jar였던 것과 달리 JUnit 5은 3개의 요소로 구성되어 있다.

JUnit Platform

: 테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API를 제공

JUnit Jupiter

: JUnit 5를 위한 테스트 API와 실행 엔진을 제공

JUnit Vintage

: 하위 호완성을 위해 JUnit 3 or 4로 작성된 테스트를 JUnit 5 플랫폼에서 실행하기 위한 모듈을 제공

 

 

JUnit 5는 테스트를 위한 API로 Jupiter API를 제공한다.

그렇기 때문에 Jupiter 관련 의존을 추가해주어야 한다.

아래는 5.9.1 버전을 기준으로 한 예시이다.

 

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.0.1'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.hou27'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
	mavenCentral()
}

dependencies {
	testImplementation 'org.junit.jupiter:junit-jupiter:5.9.1'
}

tasks.named('test') {
	useJUnitPlatform()
}

testImplementation을 사용하여 junit-jupiter 의존을 추가하고,

Test task는 JUnit 5의 플랫폼을 사용하도록 설정해 준 모습이다.

 

@Test 애노테이션과 테스트 메서드

JUnit 모듈을 설정한 후엔 테스트 코드를 작성하고 실행할 수 있게 된다.

package com.hou27.chap05;

import org.junit.jupiter.api.Test;

public class BasicTest {
  @Test
  void test() {
    // given
    // when
    // then
  }
}

 

이렇게 테스트로 사용할 클래스를 작성한 후, 메서드에 @Test만 붙여주면 된다.

여기서, @Test 애노테이션을 붙인 메서드는 private이면 안된다.

 

주요 단언 메서드

JUnit의 Assertions 클래스는 여러 단언 메서드를 제공한다.

Assertions 클래스의 모든 메소드는 static 메소드이다.

 

메서드 설명
assertEquals(expected, actual) actual 값이 expected 값과 같은지 검사
assertNotEquals(unexpected, actual) actual 값이 unexpected 값과 같지 않은지 검사
assertSame(Object expected, Object actual) 두 객체가 동일한 객체인지 검사
assertNotSame(Object unexpected, Object actual) 두 객체가 동일하지 않은 객체인지 검사
assertTrue(boolean condition) 값이 true인지 검사
assertFalse(boolean condition) 값이 false인지 검사
assertNull(Object actual) 값이 null인지 검사
assertNotNull(Object actual) 값이 null이 아닌지 검사
fail() 테스트를 실패 처리
assertThrows(Class<T> expectedType, Executable executable) executable을 실행한 결과로 지정한 타입의 exception이 발생하는지 검사
assertDoseNotThrow(Executable executable) executable을 실행한 결과로 지정한 타입의 exception이 발생하지 않는지 검사

 

Exception

다음 예시는 exception 관련 테스트를 작성한 코드이다.

public class ExceptionTest {
  @Test
  void exceptionTest() {
    assertThrows(IllegalArgumentException.class, () -> {
      throw new IllegalArgumentException();
    });
  }
}

executable 위치에 넣어준 인자가 실행되면서 expectType 위치에 지정한 타입의 exception이 발생하는지 검사한다.

 

추가로 exception을 이용해서 추가 검증이 필요하다면 아래와 같이 해준다.

public class ExceptionTest {
  ...

  @Test
  void exceptionMessageTest() {
    IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
      throw new IllegalArgumentException("message");
    });

    assertEquals("message", thrown.getMessage());
  }

}

발생한 exception 객체를 받아둔 다음, 추가로 검증해 주는 방식이다.

 

assertAll

assert 메서드는 실패하면 다음 코드는 실행하지 않고 exception을 발생시킨다.

public class assertAllTest {
  @Test
  void assertTest() {
    assertEquals(3, 8 / 3); // 검증 실패로 에러 발생
    assertEquals(3, 6 / 2); // 위 코드에서 에러 발생으로 이 코드는 실행되지 않음
    assertEquals(3, 8 / 2);
  }

}

그래서 위와 같은 경우에 두 번째 assertEquals 메서드부터는 실행되지 않는다.

결과에서 확인할 수 있듯이 3번째 assertEquals문도 틀렸지만 결과엔 나오지 않는다.

 

 

만약 실패하더라도 전부 검증해야 하는 경우엔

assertAll 메서드를 사용하면 된다.

public class assertAllTest {
  ...

  @Test
  void assertAllTest() {
    assertAll(
        () -> assertEquals(3, 8 / 3),
        () -> assertEquals(3, 6 / 2),
        () -> assertEquals(3, 8 / 2) // assertAll은 모든 검증을 실행함
    );
  }
}

이번엔 가장 마지막 라인의 테스트도 검증 실패했다고 알려주는 것으로 보아 모두 실행됐음을 확인할 수 있다.

 

테스트 라이프 사이클

@BeforeEach와 @AfterEach 애노테이션

JUnit은 각 테스트 메서드마다 다음과 같은 순서로 동작한다.

  1. 테스트 메서드를 포함한 객체 생성
  2. @BeforeEach annotation이 붙은 메서드 실행
  3. @Test annotation이 붙은 메서드 실행
  4. @AfterEach annotation이 붙은 메서드 실행

 

예시 코드

package com.hou27.chap05;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class LifecycleTest {
  public LifecycleTest() {
    System.out.println("LifecycleTest 생성자");
  }

  @BeforeEach
  void beforeEach() {
    System.out.println("beforeEach");
  }

  @Test
  void test1() {
    System.out.println("test1");
  }

  @Test
  void test2() {
    System.out.println("test2");
  }

  @AfterEach
  void afterEach() {
    System.out.println("afterEach");
  }

}

 

실행 결과

결과를 통해 확인할 수 있듯이

'Each'라는 단어를 포함한 만큼

각 테스트의 실행 전, 후에 BeforeEach와 AfterEach 애노테이션이

붙은 메서드가 실행되는 것을 알 수 있다.

 

마치 준비와 마무리를 하는 느낌이라고 생각하면 편하다.

 

@BeforeEach와 @AfterEach가 붙은 메서드는 마찬가지로 private이면 안된다.

 

@BeforeAll와 @AfterAll 애노테이션

이 둘은 All이라는 단어가 포함된 만큼

각 테스트 전후에 실행되는 것이 아니라 하나의 테스트용 클래스가

실행되기 전, 모든 테스트가 실행된 후에 동작하는 친구들이다.

 

이 둘은 static method에 적용한다.

 

테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기

공유하게 되는 경우에 의도한 대로 적절하게 검증되고 결과를 얻을 수도 있지만,

테스트 간에 공유하는 것이 있는 경우엔 높은 확률로 의도치 않은 상황이 발생되고

혼란을 야기할 수 있다.

 

예시와 함께 살펴보자

package com.hou27.chap05;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class BadTest {
  private int value = 0; // 테스트 메서드가 공유하는 인스턴스 변수

  @Test
  @DisplayName("value 값을 더하면 1이 증가한다.")
  void value_값을_더하면_1이_증가() {
    value = 1; // 테스트 메서드가 공유하는 인스턴스 변수를 사용함
    int prev = 3;
    int result = prev + value;

    assertEquals(prev+1, result);
  }

  @Test
  @DisplayName("value 값을 더하면 2가 증가한다.")
  void value_값을_더하면_2가_증가() {
    value = 2; // 테스트 메서드가 공유하는 인스턴스 변수를 사용함
    int prev = 3;
    int result = prev + value;

    assertEquals(prev+2, result);
  }


}

위와 같은 경우엔 공유하는 변수를 테스트 메서드 내에서 새로 초기화하면서 진행하기 때문에

각각의 테스트가 적절하게 동작하게 된다.

 

public class BadTest {
  private int value = 0; // 테스트 메서드가 공유하는 인스턴스 변수

  @Test
  @DisplayName("value 값을 더하면 1이 증가한다.")
  void value_값을_더하면_1이_증가() {
    value += 1; // 테스트 메서드가 공유하는 인스턴스 변수를 사용함
    int prev = 3;
    int result = prev + value;

    assertEquals(prev+1, result);
  }

  @Test
  @DisplayName("value 값을 더하면 2가 증가한다.")
  void value_값을_더하면_2가_증가() {
    value += 2; // 테스트 메서드가 공유하는 인스턴스 변수를 사용함
    int prev = 3;
    int result = prev + value;

    assertEquals(prev+2, result);
  }


}

그러나 이런 식으로 새로 초기화하는 것이 아닌 원하는 값을 더해주고,

원하는 값으로 설정되었다고 착각하고 진행하는 경우가 발생할 가능성이 매우 높다.

(예시이기 때문에 정말 단순해 보일지 몰라도, 실제 테스트 코드를 작성할 때 이런 작은 실수로 인해 엄청난 리소스 낭비가 발생할 수 있게 된다는 점을 생각해야 한다.)


참고자료

 

JUnit user-guide

테스트 주도 개발 시작하기

'Study > TDD' 카테고리의 다른 글

TDD - 대역  (0) 2023.02.08
TDD - 테스트 코드의 구성  (0) 2023.02.03
TDD - TDD ∙ 기능 명세 ∙ 설계  (2) 2023.01.16
TDD - 테스트 코드 작성 순서  (0) 2023.01.12
TDD - TDD(Test-Driven Development)란?  (0) 2023.01.06