037. (Pragmatic Unit Testing in Kotlin with JUnit) 1. 첫 JUnit 테스트 구축하기

1. 첫 JUnit 테스트 구축하기

원제 : Building Your First JUnit Test

제일 먼저 단위 테스트를 하나 작성해보며, 테스트 메서드가 어떻게 생겼는지 알아보자.

1.1. 왜 단위 테스트를 작성하는가?

원제 : Reasons to Write a Unit Test

단순히 코드만 작성하는 것과 단위 테스트를 병행하는 것은 어떤 차이가 있을까?

전자의 경우 예외나 오류가 발생하는 상황에서 코드를 수정하고 빌드하고 결과를 확인하는 과정을 전부 거쳐야 한다.

이는 피드백에 많은 시간이 소요됨을 의미한다.

반대로 단위 테스트를 작성하는 경우, 실제 실행 전에 단위 테스트를 먼저 수행하여 오류가 발생하더라도

어떤 부분에 문제가 있는지 바로 특정이 가능하고 수정 후 다시 단위 테스트를 수행하여 즉각적인 피드백을 얻을 수 있다.

이러한 단위 테스트들이 쌓이면 회귀 테스트(regressionn test) 가 되고, 최소한의 리소스 소모로 변경을 진행할 수 있게 된다.

참고 (Unit Test Principles) 1. 단위 테스트의 목표

1.2. JUnit 기초 : 첫 번째 테스트 통과

원제 : Learning JUnit Basics: Our First Passing Test

첫 예제로 ScoreCollection 이라는 클래스를 만들고 테스트해보자.

이 클래스의 목적은 이미 점수를 가지고 있는 Scoreable 객체의 컬렉션의 평균을 반환하는 것이다.

1
2
3
4
@FunctionalInterface
interface Scoreable {
fun getScore(): Int
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ScoreCollection {

private val scores = arrayListOf<Scoreable>()

fun add(scoreable: Scoreable) {
scores.add(scoreable)
}

fun arithmeticMean(): Int {
val total = scores
.map(Scoreable::getScore)
.sum()

return total / scores.size
}
}

ScoreCollection 클래스의 add() 메서드는 Scoreable 인스턴스를 매개변수로 받고,

arithmeticMean() 메서드는 입력받은 Scoreable 인스턴스들의 평균값을 계산하여 반환해준다.

1.2.1. 테스트 코드 작성

위에서 작성한 ScoreCollection 클래스의 테스트 코드인 ScoreCollectionTest 클래스를 작성해보자.

1
2
3
4
5
6
7
8
9
10
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail

class ScoreCollectionTest {

@Test
fun test() {
fail("Not yet implemented")
}
}

아직은 아무런 테스트 코드가 작성되지 않은 상태이다.

1.2.2. 테스트 코드 구조 이해

위에서 작성한 테스트 코드의 구조를 알아보자.

1
2
3
4
5
6
7
8
9
10
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail

class ScoreCollectionTest { // 대부분 타겟 클래스에 접미사 Test를 붙여서 클래스를 만든다. (표준)

@Test // Test 어노테이션이 붙은 메서드들만을 대상으로 테스트를 실행한다.
fun test() {
fail("Not yet implemented") // fail을 쓰면 무조건 테스트가 실패하도록 동작한다.
}
}

1.2.3. 테스트 코드 실행

위의 테스트를 실행한다면 아래와 같은 결과가 출력된다.

1
2
3
4
5
6
7
8
org.opentest4j.AssertionFailedError: Not yet implemented
at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:43)
at org.junit.jupiter.api.Assertions.fail(Assertions.java:146)
at org.junit.jupiter.api.AssertionsKt.fail(Assertions.kt:27)
at org.junit.jupiter.api.AssertionsKt.fail$default(Assertions.kt:26)
at ch01.ScoreCollectionTest.test(ScoreCollectionTest.kt:10) <29 internal lines>
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) <9 internal lines>
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511) <27 internal lines>

1.3. 테스트를 위한 방법 - AAA 패턴

원제 : Arrange, Act, and Assert Your Way to a Test

이제 테스트 코드가 정상 동작하도록 코드를 작성해보자.

정상 동작을 위한 시나리오를 하나 생각해보자.

위의 코드의 로직을 통해 score로 5와 7이 주어지면 평균 값은 6을 반환할 것임을 추측할 수 있다.

이 시나리오를 테스트 코드로 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ScoreCollectionTest {

@Test
fun answersArithmeticMeanOfTwoNumbers() {
// Arrange
val collection = ScoreCollection()
with(collection) {
add(object : Scoreable {
override fun getScore() = 5
})
add(object : Scoreable {
override fun getScore() = 7
})
}

// Act
val actualResult = collection.arithmeticMean()

// Assert
assertEquals(actualResult, 6)
}
}

위의 코드에서 Arrange, Act, Assert 라는 주석을 확인할 수 있는데, 이를 AAA 패턴이라고 한다.

각각 테스트 준비 영역, 테스트 실행 영역, 테스트 검증 영역으로 코드를 작성하는 스타일을 의미한다.

이 테스트는 지금 성공하고 있지만, arithmeticMean()메서드의 동작이 변경되면 바로 실패함으르서, 빠른 피드백을 보여줄 것이다.

참고 (Unit Test Principles) 3. 단위 테스트의 구조

1.4. 테스트는 정말로 무언가를 테스트하고 있는가?

원제 : Is the Test Really Testing Anything?

테스트는 항상 성공해야할까?

fail을 쓰는 것처럼 의도적으로 실패하는 테스트를 작성할 수도 있다.

다만 이런 테스트는 항상 실패하고 있는지 꼼꼼히 확인하는 습관을 들여야 한다.

참고 (Unit Test Principles) 4. 좋은 단위 테스트의 4대 요소