Junit5를 사용한 Java 테스트 코드 작성

안녕하세요. 오늘은 유닛 테스트에 대한 이야기를 해보고자 합니다. 프로그래밍 하다보면 자신이 작성한 코드가 원하는 형태로 작성되었는지 안되었는지를 테스트해보고 싶을 때가 있습니다. 저는 처음 프로그래밍을 GUI 프로그래밍으로 했다보니 그 결과를 보통 눈으로 확인하곤 했습니다.

하지만 CLI 프로그래밍을 하다보면 원하는 결과가 나오는지 안나오는지 일일이 손으로 입력하고 결과를 본다는 것이 쉽지만은 않습니다. 그럴 때 유닛 테스트를 사용해보세요.

What is Unit Test ?

그렇다면 유닛 테스트는 무엇인가요? 유닛 테스트는 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도대로 정확히 작동하는지 검증하는 일련의 절차 과정입니다. 좀 더 자세한 설명을 위해서 예시를 한 번 들어보도록 할게요.

여러분들이 어떤 알고리즘 코드를 작성한다고 가정해봅시다. 그 알고리즘은 숫자를 입력 받아 해당 숫자가 팰린드롬인지 아닌지를 작성하는 알고리즘 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Solution {
public boolean solution(int num) {
if (num < 0 || (num % 10 == 0 && num != 0))
return false;

int rh = 0;

while (num > rh) {
rh = rh * 10 + num % 10;
num /= 10;
}

return num == rh || num == rh / 10;
}
}

이러한 코드를 작성했다고 했을 때, 이 코드가 정상적으로 동작하는지 확인하려면 보통은 아래와 같이 Entry Point를 사용하여 직접 돌려보는 방법을 사용할 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Solution {
public static boolean solution(int num) {
if (num < 0 || (num % 10 == 0 && num != 0))
return false;

int rh = 0;

while (num > rh) {
rh = rh * 10 + num % 10;
num /= 10;
}

return num == rh || num == rh / 10;
}

public static void main(String[] args) {
int num = 1111;
System.out.println(solution(num));
}
}

하지만 이러한 코드는 그저 이 로직이 돌아가는지 돌아가지 않는지만 평가해볼 수 있습니다. 이 값이 제대로 도출되는지 도출되지 않는지까지는 파악하기 어렵죠. 그래서 우리는 조건문을 사용해볼 수 있습니다.

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
public class Solution {
public static boolean solution(int num) {
if (num < 0 || (num % 10 == 0 && num != 0))
return false;

int rh = 0;

while (num > rh) {
rh = rh * 10 + num % 10;
num /= 10;
}

return num == rh || num == rh / 10;
}

public static void main(String[] args) throws Exception {
int[] num = {1111, 1412, 2020, 1212};
boolean[] evl = {true, false, true, true};

for (int i = 0; i < num.length; i++) {
if (solution(num[i]) != evl[i])
System.err.println("FAIL: " + i);
else
System.out.println("SUCCESS: " + i);
}
}
}

위와 같은 형태로 코드를 테스트하게 되면 원하는 답이 제대로 도출되는지도 확인할 수 있죠. 하지만 이 작업은 뭔가 번거롭고 알고리즘이 조금이라도 복잡해지는 등이 발생하게 되면 오히려 테스트 코드 작성에 더 많은 비용이 들어갈 수도 있습니다.

Junit

Java에서는 Junit이라는 모듈을 사용하여 테스트 코드를 간단한 로직만으로도 작성할 수 있습니다. 오늘은 이 모듈을 사용하여 Java에서 테스트 코드를 작성하고 활용하는 방법에 대해 이야기 하려 합니다. 이 포스트에서 사용할 IDE는 아래와 같습니다.

  • IntelliJ IDEA

Create Program code

먼저 간단히 사용 방법에 대해 알아보겠습니다.

Junit-1

평상 Java 애플리케이션을 개발할 때처럼 일반 프로젝트를 하나 생성합니다.

Junit-2

그러면 기본적으로 src 디렉터리가 생기게 됩니다. 이 곳에는 여러분들이 작성한 소스 코드가 들어가게 됩니다. 거기에 추가로 디렉터리를 만들어서 아래와 같은 형태가 되도록 해봅시다.

Junit-3

tests 디렉터리를 생성하고, 프로젝트 오른쪽 클릭하여 Open Module Settings를 클릭하면 위와 같은 화면이 나타납니다. tests 디렉터리를 클릭하고, Mark as tests 까지 클릭해주면 해당 디렉터리는 이제 test 코드를 위한 디렉터리로 인식하게 됩니다.

Junit-4

이제 src 디렉터리에 여러분들이 테스트하고자 하는 로직을 작성해줍니다. 저는 예시를 사용하여 위에서 사용한 팰린드롬 알고리즘을 작성해봤습니다.

Junit-5

알고리즘 작성이 끝나셨다면 코드 맨 위에 커서를 갔다대보세요. 그러면 위 사진같이 전구 모양의 아이콘이 활성화 됩니다. 그 버튼을 클릭하셔서 Create Test 버튼을 클릭하시면 테스트 코드 작성 창이 뜨게 됩니다.

Create Test code

여기서부터는 테스트 코드를 작성하기 위한 가이드라인입니다.

Junit-6

여기서는 Junit5 버전을 사용하여 테스트 코드를 작성해볼 것입니다. 따라서 라이브러리를 Junit5 모듈로 맞춰주시고, 처음에는 모듈이 설치되어 있지 않으므로 Fix 버튼을 눌러 모듈을 설치해줍니다. 그러면 Maven에서 IDE가 자동으로 모듈을 다운로드 받아 설치해줍니다.

Junit-7

설치가 끝났으면 테스트에 사용할 메소드와 그리고 @BeforeAll를 추가주도록 합시다.

@BeforeAll

Junit5에서 BeforeAll 어노테이션은 Junit4에서 @Before 어노테이션과 같은 역할을 합니다. 테스트 코드를 수행하기 전에 실행할 코드를 정의하는 LifeCycle 중 하나로, 반대의 어노테이션으로 @After가 있습니다

@BeforeEach 어노테이션은 각각의 Test Method 당 실행하기 전 수행할 작업을 일컫는 반면, @BeforeAll은 현재 클래스에서 모든 메소드가 실행하기 전에 나타납니다. 즉, Each는 메소드 각각 수행하고, All은 한 번 수행한다는 것이죠.

우리가 사용할 것은 @BeforeAll 입니다. 왜냐하면, 테스트 코드 실행 전에 객체를 생성해야 하기 때문이죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

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

/**
* Created by neonkid on 9/26/19
*/

class AlgorithmTest {

@BeforeAll
void setUp() {
// 여기에 테스트 코드 수행 전 실행할 작업을 작성합니다.
}

@Test
void solution() {
// 여기에 테스트할 코드를 작성합니다.
}
}

이렇게 테스트 코드의 기본 형태가 완성됩니다.

Write Test code

이제 테스트할 코드를 작성해봅시다. 테스트는 총 2가지 테스트를 작성해볼 것입니다.

  1. 정확도 테스트
  2. 효율성 테스트

정확도 테스트는 특정 숫자를 정적으로 입력하고, 해당 테스트의 결과를 비교하여 정확하게 답을 맞췄는지 확인해보는 테스트이고, 효율성 테스트는 이 문제를 해결하는 데 얼마만큼의 시간을 소모하는지를 진행해보는 테스트입니다.

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
31
32
33
34
35
36
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.time.Duration;

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

/**
* Created by neonkid on 9/26/19
*/

class AlgorithmTest {
private static Algorithm a;
private static int[] num = {1412, 2222, 1010, 3045, 10001000, 3129430, 3333};
private static boolean[] evl = {true, true, false, false, false, false, true};

@BeforeAll
static void setUp() {
a = new Algorithm();
}

@DisplayName("정확도 테스트")
@Test
void solution() {
for (int i = 0; i < num.length; i++)
assertEquals(a.solution(num[i]), evl[i]);
}

@DisplayName("효율성 테스트")
@Test
void tle() {
for (int q : num)
assertTimeout(Duration.ofMillis(0), () -> a.solution(q));
}
}

테스트 코드는 이렇게 작성해볼 수 있습니다. 위에서 작성한 EntryPoint와는 큰 차이가 없습니다. 다만 조건문을 제거하고, assertEquals라는 비교 메소드를 사용하면서 각각의 테스트를 해볼 수 있습니다. 추가로 assertTimeout 메소드를 통해 정해진 시간을 지정해놓고, 해당 시간을 초과하면 효율성 테스트 미달의 판정을 내릴 수도 있죠.

하지만 이렇게 테스트 코드를 작성하면 단점이 존재합니다.

Junit-8

위 이미지는 테스트 코드를 실행한 화면입니다. 위와 같이 IDE 창 아랫 부분에 각 테스트 결과가 표시되고, 테스트 결과가 온전하면 초록색 체크 아이콘을, 테스트가 실패한 경우에는 오류를 내뿜습니다. 테스트 코드 중 1412는 팰린드롬 숫자가 아닙니다. 거꾸로 읽으면 2141이 되기 때문이죠. 그런데 true라고 제가 테스트 코드를 주었기 때문에 오류를 뿜은 것입니다.

지금과 같은 경우는 이미 알고 테스트 코드에 오류가 있음을 알 수 있었지만 이와 같이 반복문을 사용한 코드는 코드의 가독성을 올려주는 효과가 있습니다. 하지만 반대로 테스트 코드에서는 어느 테스트 값이 오류인지를 찾기가 어렵죠.

Junit-9

그래서 이 때는 정적의 값을 사용해야 합니다. (물론 코드가 좀 많이 지저분해지죠 ㅠㅠ) 하지만 위의 사진과 같이 어느 부분에 오류가 있는지를 명확히 표시해줍니다. 이 때 사용하는 메소드는 assertAll 메소드로 Junit 5에서 사용이 가능합니다.

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.time.Duration;

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

/**
* Created by neonkid on 9/26/19
*/

class AlgorithmTest {
private static Algorithm a;

@BeforeAll
static void setUp() {
a = new Algorithm();
}

@DisplayName("정확도 테스트")
@Test
void solution() {
assertAll("Heading",
() -> assertFalse(a.solution(1412)),
() -> assertTrue(a.solution(2222)),
() -> assertFalse(a.solution(1010)),
() -> assertFalse(a.solution(3045)),
() -> assertFalse(a.solution(10001000)),
() -> assertFalse(a.solution(3129430)),
() -> assertTrue(a.solution(3333)));
}

@DisplayName("효율성 테스트")
@Test
void tle() {
assertAll("Heading",
() -> assertTimeout(Duration.ofMillis(0), () -> a.solution(1412)),
() -> assertTimeout(Duration.ofMillis(0), () -> a.solution(2222)),
() -> assertTimeout(Duration.ofMillis(0), () -> a.solution(1010)),
() -> assertTimeout(Duration.ofMillis(0), () -> a.solution(3045)),
() -> assertTimeout(Duration.ofMillis(0), () -> a.solution(10001000)),
() -> assertTimeout(Duration.ofMillis(0), () -> a.solution(3129430)),
() -> assertTimeout(Duration.ofMillis(0), () -> a.solution(3333)));
}
}

이제 정확한 테스트 코드로 바꿔보고, 한 번 실행해보겠습니다.

Junit-10

그러면 깔끔하게 모든 테스트 결과가 잘 되고 있음을 알 수 있습니다. 코드를 수정하는 일은 빈번하고 실수는 누구나 하기 마련입니다. 이러한 테스트 코드를 잘만 짜둔다면 자신의 코드를 보다 더 효율적으로 품질 높은 상태로 관리할 수 있죠.

마치며…

여기까지 Junit 5를 사용한 테스트 코드 작성이었습니다. 사실 이 글을 작성하는 데 있어 많은 고민을 했습니다. 프로그래머로써 테스트 코드를 작성하는 건 매우 중요한 일이지만 사람에 따라서 이러한 테스트 모듈을 쓸 수도 있고 안쓸 수도 있기 때문입니다.

최근 제가 알고리즘 연습을 하게 되면서 테스트 모듈을 자주 사용하고 있습니다. 백엔드를 개발할 때는 보통 Mock up 서버를 써서 테스트하는 경우도 종종있지만 Gradle을 사용해서 테스트 코드를 직접 실행하여 결과를 확인하고, 배포하는 것이 가장 쉬운 방법이기도 하니깐요.

이 방법을 응용하면 Spring / Spring boot에서 WAS로 배포할 때 테스트 코드를 진행 후 배포하는 방향으로도 개발 환경을 갖출 수도 있습니다. 차후에는 위의 주제로 한 번 포스팅할 예정입니다.

0%