[Test] 코틀린에서 JUnit 5를 사용해 테스트코드 작성하기 기초
이번 포스팅에서는 JUnit 5를 사용해 코틀린에서 테스트코드를 어떻게 작성하는지 알아보겠습니다. 저는 재작년 우테코 6기 프리코스를 할 때 테스트코드라는 것을 처음 알게 되었고, 작성해 보았는데요! 난생처음 작성해 보는 데다 관련 정보가 많지 않아서 테스트코드를 짜기 굉장히 어려웠던 기억이 있습니다. 오늘은 제가 우테코에서 배운 JUnit 5 사용법을 기초부터 설명해 보려고 합니다. 테스트코드가 처음이신 분들도 쉽게 따라올 수 있도록 최대한 쉽게 설명하려고 하니, 테스트코드에 대한 이해가 없는 분들께 도움이 되었으면 좋겠습니다! 😊
1. JUnit 5란?
테스트코드를 작성하고 싶어서 들어왔는데 JUnit 5 얘기를 계속 하네? 나는 아직 JUnit 5가 뭔지도 모르는데.. 하시는 분들이 계실 수 있습니다. 저도 처음에 그랬죠 ㅎㅎ 테스트코드를 작성하기 전에 JUnit이 무엇인지부터 알아야겠습니다.
JUnit 5란, 자바 언어에서 단위테스트(Unit Test)를 할 수 있도록 해 주는 테스팅 프레임워크입니다. 프레임워크란, 개발자가 어떤 작은 작업을 할 수 있도록 도와주는 작은 프로그램이라고 생각하시면 됩니다. 개발자용 프로그램인 셈이죠. 그리고 JUnit은 개발자가 작성한 코드가 의도대로 동작하는지를 테스트하게 해 줍니다.
단위테스트(Unit Test)란? 프로그램 전체를 테스트하는 것이 아닌 독립된 작은 단위의 기능만을 테스트하는 것을 말합니다. 단위테스트에 대해서는 다른 포스팅에서 더 다루도록 하겠습니다.
JUnit 5라는 이름에서 알 수 있듯이, JUnit 5는 JUnit 프레임워크 시리즈의 다섯 번째 버전입니다. JUnit 1.0이 1997년 출시되었고 다양한 기능이 추가되며 업그레이드가 계속되었고, 현재 5번째 버전이 최신버전으로 출시되어 있습니다. 현재는 대다수의 개발자가 최신버전인 Junit 5를 쓴다고 생각하시면 되겠습니다!
그런데 자바 언어용 프레임워크라면 코틀린에서도 사용 가능한 걸까요? 물론입니다! 코틀린은 자바와 100% 호환이 되고, 자바와 같이 JVM 위에서 동작하기 때문에 컴파일 시 JVM 바이트코드로 변환되지요. 자바에서와 마찬가지로 JUnit 5의 사용이 전혀 문제가 없습니다. 😁
2. 사전세팅
그럼 본격적으로 JUnit 5를 사용해 보겠습니다. 먼저 시작하기 전에, 간단한 세팅을 하도록 하겠습니다.
왼쪽 이미지처럼 build.gradle.kts을 찾아 열어주세요. 그럼 오른쪽과 같은 코드가 나올 텐데요! 13~15번 라인처럼 dependencies
안에 testImplementation(kotlin("test"))
가 들어있는지 확인해 주세요! 만약 들어있지 않다면, 다음의 코드를 추가해 줍니다.
testImplementation("org.junit.jupiter", "junit-jupiter", "5.11.4")
위 코드는 JUnit 5의 외부 의존성을 우리 프로젝트에 추가한 것입니다. JUnit 5는 코틀린에 내장되어있지 않은 외부 프레임워크이기 때문에, 우리 프로젝트에 설치해야 사용할 수 있습니다. 위의 코드를 dependencies
에 추가하면, JUnit 5가 설치가 된 것입니다.
참고로, jupiter는 JUnit의 다섯번째 버전, 즉 JUnit 5를 의미합니다. 또 5.11.4라는 숫자는 사용할 버전을 의미하는데요, 이 글을 쓰는 25년 1월 12일 기준으로 JUnit의 최신버전은 5.11.4입니다. (구버전을 사용해도 사실 큰 문제는 없을 거예요)
사실 프로젝트를 처음 생성하면 기본적으로 testImplementation(kotlin("test"))
가 이미 들어있을 겁니다. 이 코드는 코틀린 표준 라이브러리의 테스트 모듈을 가져오는데요, 이 테스트모듈에 JUnit 5가 기본적으로 포함되어 있기 때문에 의존성을 꼭 추가하지 않아도 JUnit 5를 사용할 수 있습니다.
그리고 사실 JUnit 5를 효과적으로 사용하기 위해서는 외부 라이브러리 하나가 더 필요한데요, 바로 AssertJ입니다. 이 라이브러리가 무슨 역할을 하는지는 아래에서 살펴보겠습니다. AssertJ를 사용하기 위해 다음의 코드도 dependencies
에 추가합시다. JUnit 5와 마찬가지로 최신버전인 3.27.2를 쓰겠습니다.
testImplementation("org.assertj", "assertj-core", "3.27.2")
자, 여기까지 완료되었다면 Gradle 파일의 dependencies
부분이 아래와 같게 바뀌었을 텐데요,
dependencies {
testImplementation(kotlin("test"))
testImplementation("org.assertj", "assertj-core", "3.27.2")
}
또는
dependencies {
testImplementation("org.junit.jupiter", "junit-jupiter", "5.11.4")
testImplementation("org.assertj", "assertj-core", "3.27.2")
}
완료되었다면, 화면 오른쪽 위에 나타난 코끼리 새로고침 아이콘을 클릭해 Gradle을 Sync 해줍니다. 이 아이콘을 클릭해 주어야 변경이 적용이 되거든요!
여기까지 되었으면 테스트코드를 작성할 준비가 끝난 것입니다! 👍
3. 테스트코드 작성하기
그럼 이제 진짜로 테스트코드를 작성해 볼 시간입니다.
위 이미지대로, src/test/kotlin 폴더에 들어가 Kotlin Class/File을 하나 만들어줍니다. 저는 MyTest라는 이름으로 만들었습니다.
그리고 다음의 예제를 입력해 보시죠!
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class MyTest {
@Test
fun `증감 후위 연산자는 계산 이전 값을 반환한다`() {
// given
var a: Int = 3
// when
val actual = a++
// then
assertThat(actual).isEqualTo(3)
}
}
간단한 예제인데요, 후위 연산자가 어떻게 동작하는지에 대한 테스트코드를 작성해 보았습니다. 잘 모르겠는 게 여러 개 있으니 하나씩 살펴볼게요.
먼저 @Test
라는 어노테이션이 붙은 것을 보실 수 있을 겁니다. JUnit 5는 이 @Test
어노테이션이 붙은 함수가 테스트함수라는 것을 알고, 개발자가 테스트코드를 실행시키면 이 함수들을 실행합니다.
4번 라인과 6번 라인에 초록색 실행 아이콘이 생긴 것을 볼 수 있죠? 저것을 클릭하면 테스트코드를 실행할 수 있습니다. class 앞에 붙은 것은 이 클래스 안의 모든 테스트함수를 실행하고요, fun 앞에 붙은 것은 그 함수만 실행합니다.
실행하게 되면 위와 같은 창이 팝업 됩니다. 그리고 테스트가 성공했는지 실패했는지를 확인할 수 있어요! "Tests passed 1 of 1 test"라는 글씨가 보이시죠? 1개의 테스트가 존재하고 그중 1개가 통과했다는 뜻입니다. 😁
그런데 무엇을 기준으로 테스트 통과, 실패를 결정하는 것인지는 아직 설명하지 않았죠? 코드를 다시 살펴봅시다!
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class MyTest {
@Test
fun `증감 후위 연산자는 계산 이전 값을 반환한다`() {
// given
var a: Int = 3
// when
val actual = a++
// then
assertThat(actual).isEqualTo(3)
}
}
여기서 함수의 마지막 줄을 보면 assertThat()
함수와 isEqualTo()
함수를 사용한 것을 보실 수 있을 겁니다. 이 함수는 AssertJ 라이브러리에서 제공하는 함수인데요, 이 함수를 통해 우리가 원하는 테스트를 작성할 수 있습니다. 위에서 AssertJ의 의존성을 Gradle에 추가한 이유가 이것입니다. 이 assertThat()
함수와 isEqualTo()
함수를 사용하기 위함이지요.
AssertJ를 반드시 써야 하나?
사실 JUnit 5를 사용할 때 AssertJ가 필수인 것은 아닙니다. JUnit 5만으로도 테스트코드를 작성할 수 있어요. 하지만 AssertJ를 JUnit 5와 같이 사용하는 이유는 더 직관적이고 유연한 단언문 작성을 위해서입니다.
자세히 알아보기
"assert"는 한국말로 번역하면 "단언하다"라는 뜻입니다. 이 함수의 마지막줄을 한국말로 번역하자면, "나는 actual
이 3
과 같다고 단언한다"라는 의미입니다. 그리고 테스트함수는 이 단언이 참인지 거짓인지를 기준으로 테스트 통과/실패를 결정합니다. 즉 actual
이 3
과 같으면 내 단언이 참이기 때문에 테스트가 통과하고, actual
이 3
과 같지 않다면 거짓이므로 테스트가 실패하게 되는 것입니다. 참고로 "actual"이라는 네이밍은 단언문에서 실제 값과 기대 값(여기서는 3이 되겠네요)을 비교할 때 실제 값이라는 의미입니다. 테스트코드에서 관례적으로 많이 사용되는 네이밍입니다.
이 함수에서 저는 a
라는 Int형 변수를 선언했고 3을 저장했습니다. 그리고 actual
에 a++
를 저장했네요. 그럼 actual
에는 무엇이 저장되어 있을까요? ++ 후위 연산을 썼기 때문에 증가하기 전 값인 3이 저장됩니다. 따라서 저의 단언은 참이고, 테스트가 통과한 것이겠네요! 😆
즉 이 테스트를 한 문장으로 설명하자면, "내 의도대로라면 a = 3일 때 a++는 3과 같다. 실제 프로그램이 내 의도대로 동작하는지 테스트하자"라고 볼 수 있습니다.
만약 다음과 같이 3 대신 4를 써서 거짓인 단언문을 작성했다면 테스트가 어떻게 되는지도 볼까요?
보시다시피 테스트가 실패하고, expected:4, but was: 3이라는 메시지가 출력됨을 확인할 수 있습니다. 저는 actual
이 4
라고 기대했는데, 실제로는 3이었다는 것이죠. 테스트가 실패하면 이 메시지를 통해 실제 값이 무엇인지도 확인할 수 있습니다.
마지막으로 given, when, then이라는 주석에 대해서 잠깐 이야기해보자면, 이 세 가지 주석은 테스트코드를 명확하고 직관적인 방식으로 작성하기 위한 하나의 가이드라인입니다. given에는 테스트를 하기 위한 상황을, when에는 테스트 대상의 동작을, then에는 결과를 검증하는 코드를 작성하게 됩니다. 많은 개발자들이 테스트코드에서 given, when, then 주석을 사용하고 있으니 처음부터 습관을 들여두시길 권장드립니다.
그럼 테스트를 여러 개 작성하고 싶다면 어떻게 하면 될까요? 클래스에 테스트함수를 더 추가하면 됩니다.
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class MyTest {
@Test
fun `증감 후위 연산자는 계산 이전 값을 반환한다`() {
// given
var a: Int = 3
// when
val actual = a++
// then
assertThat(actual).isEqualTo(3)
}
@Test
fun `증감 전위 연산자는 계산 이후 값을 반환한다`() {
// given
var a: Int = 3
// when
val actual = ++a
// then
assertThat(actual).isEqualTo(4)
}
}
그리고 class 옆의 실행 버튼을 클릭하면 이 클래스 안의 모든 테스트함수를 실행할 수 있는 겁니다!
두 개 모두 성공~~
어때요, 제 글을 읽고 테스트코드에 대한 이해가 조금 되셨을까요? 테스트를 처음 짜서 막막하던 시절의 저에게 알려준다는 느낌으로 적어보았습니다. 저는 처음에 단언이라는 개념이 잘 와닿지 않았던 기억이 있었네요. 😂 그래서 그 부분에 대해서 자세히 설명을 남겨보았습니다. 이해가 되었으면 좋겠네요!
이번 글에서 제가 소개한 예제는 이해를 돕기 위한 간단한 예제였고, 실제 테스트코드에서는 보통 개발자가 설계한 도메인 객체의 기능에 대해서 테스트를 작성하게 된답니다. 그리고 이것을 단위테스트라고 하지요. 단위테스트에 대해서 자세히 알아보려면 코틀린에서 JUnit 5를 사용해 단위테스트 하기를 읽어주세요! 😁