0. 들어가며

스프링 스터디를 진행하면서 테스트를 다룬적이 있지만 JUnit이나 테스트의 개념 정도만 정처기 공부할때 다뤄봤지 따로 코드로 작성해본적은 없어서 이해하기 어려웠다. 그래서 이번에 제대로 이해하고 앞으로의 개발에도 적용할 수 있도록 연습해보려고 한다.

1. 단위 테스트란 무엇일까?

단위 테스트 : 작은 단위로 쪼개서 각 단위가 정학하게 동작하는지 검사하는 테스트 기법

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
 
tasks.named('test') {
    useJUnitPlatform()
}
  • JUnit 관련 dependency들은 프로젝트 생성시 자동으로 설정된다

먼저 JUnit에서 제공하는 기능들을 몇개 확인해보자

Before - After

public class BeforeAfterTest {
 
    @BeforeEach
    void setUp() {
        System.out.println("각각의 테스트 코드가 실행되기 전에 수행");
 
    }
 
    @AfterEach
    void tearDown() {
        System.out.println("각각의 테스트 코드가 실행된 후에 수행\n");
    }
 
    @BeforeAll
    static void beforeAll() {
        System.out.println("모든 테스트 코드가 실행되기 전에 수행\n");
    }
 
    @AfterAll
    static void afterAll() {
        System.out.println("모든 테스트 코드가 실행된 후에 수행");
    }
 
    @Test
    void test1() {
        System.out.println("테스트 코드 1");
    }
 
    @Test
    void test2() {
        System.out.println("테스트 코드 2");
    }
 
}

각각의 테스트 코드가 실행되기 전/후에 뭐가 필요하다면? BeforeEach / AfterEach

최초로/마지막으로 뭐가 필요하다면? BeforeAll / AfterAll

@DisplayName, @Nested, @Order

@Test
@DisplayName("테스트의 내용을 한눈에 알아볼 수 있게 네이밍 해줄 때")
void test1() {
    System.out.println("테스트 내용 빠르게 파악");
}
@Nested
@DisplayName("주제 별로 테스트를 그룹지어서 파악하기 좋습니다.")
class Test1 {
    @Test
    @DisplayName("Test1 - test1()")
    void test1() {
        System.out.println("Test1.test1");
    }
 
    @Test
    @DisplayName("Test1 - test2()")
    void test2() {
        System.out.println("Test1.test2");
    }
}
 
@Nested
@DisplayName("Test2 다른 주제")
class Test2 {
    @Test
    @DisplayName("Test2 - test1()")
    void test1() {
        System.out.println("Test2.test1");
    }
 
    @Test
    @DisplayName("Test2 - test2()")
    void test2() {
        System.out.println("Test2.test2");
    }
}
  • 테스트 코드를 한 파일에서 그룹별로 묶어서 작성할 때 사용하면 유용하다
@Nested
@DisplayName("주제 별로 테스트를 그룹지어서 파악하기 좋습니다.")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class Test1 {
 
    @Order(1)
    @Test
    @DisplayName("Test1 클래스")
    void test() {
        System.out.println("\nTest1 클래스");
    }
 
    @Order(3)
    @Test
    @DisplayName("Test1 - test1()")
    void test1() {
        System.out.println("Test1.test1");
    }
 
    @Order(2)
    @Test
    @DisplayName("Test1 - test2()")
    void test2() {
        System.out.println("Test1.test2");
    }
}
  • Order()를 사용하면 테스트 순서 지정이 가능하다

@RepeatedTest

@RepeatedTest(value = 5, name = "반복 테스트 {currentRepetition} / {totalRepetitions}")
void repeatTest(RepetitionInfo info) {
    System.out.println("테스트 반복 : " + info.getCurrentRepetition() + " / " + info.getTotalRepetitions());
}
  • 테스트를 for문처럼 사용 할 수 있다

@ParameterizedTest

@DisplayName("파라미터 값 활용하여 테스트 하기")
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9})
void parameterTest(int num) {
    System.out.println("5 * num = " + 5 * num);
}
  • 파라미터 값을 이용해서 반복 테스트를 할 수 있다

이제 Assertion 테스트를 진행해보자

AssertionEquals(expected, actual)

expected = 테스트 후 기댓값 actual = 실제 결과값

Calculator calculator;
@BeforeEach
void setUp() {
    calculator = new Calculator();
}

테스트 전 계산기 기능을 하는 클래스를 하나 만들고 아까 배운대로 @BeforeEach를 사용해서 테스트를 진행할때마다 Calculator 객체를 만들어서 사용한다

AssertEquals

@Test
@DisplayName("assertEquals")
void test1() {
    Double result = calculator.operate(5, "/", 2);
    assertEquals(2.5, result);
}

expected에는 테스트 결과로 나올 기댓값을 넣는다 expected 값과 result 값이 같으면 문제 없이 테스트가 완료된다

@Test
@DisplayName("assertEquals - Supplier")
void test1_1() {
    Double result = calculator.operate(5, "/", 0);
    // 테스트 실패 시 메시지 출력 (new Supplier<String>())
    assertEquals(2.5, result, () -> "연산자 혹은 분모가 0이 아닌지 확인해보세요!");
}

Supplier를 이용하면 테스트에 실패했을 때 메시지를 출력할 수 있다

@Test
@DisplayName("assertNotEquals")
void test1_2() {
    Double result = calculator.operate(5, "/", 0);
    assertNotEquals(2.5, result);
}

AssertTrue , AssertFalse

Calculator class에는 다음과 같은 메소드가 포함되어 있다

public boolean validateNum(double num) {
    if (num == 0) {
        return false;
    } else {
        return true;
    }
}
@Test
@DisplayName("assertTrue 와 assertFalse")
void test2() {
    assertTrue(calculator.validateNum(9));
    assertFalse(calculator.validateNum(0));
}

assertTrue는 calculator.validationNum(9)가 true인지 아닌지 (assertFalse는 반대) 테스트 할 수 있다

AssertNull

@Test
@DisplayName("assertNotNull 과 assertNull")
void test3() {
    Double result1 = calculator.operate(5, "/", 2);
    assertNotNull(result1);
    Double result2 = calculator.operate(5, "/", 0);
    assertNull(result2);
}

null도 체크 가능

AssertThrows

@Test
@DisplayName("assertThrows")
void test4() {
    IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> calculator.operate(5, "?", 2));
    assertEquals("잘못된 연산자입니다.", exception.getMessage());
}

예상하는 exception을 테스트 할 수 있다


Given-when-then 패턴

Test code 스타일을 표현하는 방식

협업하는 과정에서 테스트 코드를 작성하는 방식이 모두 다르면 문제가 될 수 있기 때문에 테스트 코드를 작성하는 패턴이 생겨났고 given-when-then 패턴은 그 중 하나이다

given : 테스트 하고자 하는 대상을 실행하기 전 필요한 값들을 미리 선언 when : 테스트 하고자 하는 대상을 실제로 실행 then : 어떤 특정한 행동 때문에 발생할거라고 예상되는 결과에 대해 예측하고 맞는지 확인 (Assertion)