Kotlin과 JUnit으로 Java 코드 테스트하기 – 튜토리얼

Kotlin과 JUnit으로 Java 코드 테스트하기 – 튜토리얼

Kotlin은 Java와 완벽하게 상호운용돼요. 그래서 Java 코드를 Kotlin으로 테스트를 작성해서, 같은 프로젝트 안에서 기존 Java 테스트와 함께 실행할 수 있답니다.

이 튜토리얼에서는 다음을 배워볼게요.

  • Java–Kotlin 혼합 프로젝트를 구성해서 JUnit으로 테스트를 실행하는 법
  • Java 코드를 검증하는 Kotlin 테스트를 추가하는 법
  • Maven이나 Gradle로 테스트를 실행하는 법

참고: 시작하기 전에 준비물을 확인하세요.

프로젝트 구성하기

  • IDE에서 버전 관리로부터 샘플 프로젝트를 클론해요.
https://github.com/kotlin-hands-on/kotlin-junit-sample.git
  • initial 모듈로 이동해서 프로젝트 구조를 살펴봐요.
kotlin-junit-sample/
├── initial/
│   ├── src/
│   │   ├── main/java/    # Java source code
│   │   └── test/java/    # JUnit test in Java
│   ├── pom.xml           # Maven configuration
│   └── build.gradle.kts  # Gradle configuration

initial 모듈에는 단일 테스트가 있는 간단한 Java Todo 애플리케이션이 들어 있어요.

  • 같은 디렉터리에서 빌드 파일을 열고, Kotlin을 지원하도록 내용을 업데이트해요.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-junit-complete</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>kotlin-junit-complete</name>
    <url>https://kotlinlang.org/docs/jvm-test-using-junit.htm</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>17</maven.compiler.release>
        <jexer.version>1.6.0</jexer.version>
        <kotlin.version>2.4.20</kotlin.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>6.0.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Add JUnit Jupiter engine for test runtime -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Optionally: parameterized tests support -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.gitlab.klamonte</groupId>
            <artifactId>jexer</artifactId>
            <version>${jexer.version}</version>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement><!-- Lock down plugin versions to avoid using Maven defaults (can be moved to a parent pom file) -->
            <plugins>
                <!-- Clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.4.0</version>
                </plugin>
                <!-- Default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.3.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>3.3.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.4.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>3.1.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>3.1.2</version>
                </plugin>
                <!-- Site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
                <plugin>
                    <artifactId>maven-site-plugin</artifactId>
                    <version>3.12.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-project-info-reports-plugin</artifactId>
                    <version>3.6.1</version>
                </plugin>
                <!-- No maven-compiler-plugin needed with Kotlin extensions -->
            </plugins>
        </pluginManagement>
        <plugins>
            <!-- Activate Kotlin Maven plugin for main and test sources -->
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <extensions>true</extensions>
            </plugin>
        </plugins>
    </build>
</project>
  • <properties> 섹션에서 Kotlin 버전을 설정해요.
  • <dependencies> 섹션에서 테스트 실행을 위한 JUnit Jupiter 의존성을 추가해요.
  • <build><plugins> 섹션에서 <extensions>true로 설정한 kotlin-maven-plugin을 적용해요. 이 플러그인은 빌드에 해당 실행(execution)과 kotlin-stdlib 의존성을 자동으로 추가해 줘요.
  • Kotlin Maven 플러그인을 확장(extensions)과 함께 쓸 때는 <build><pluginManagement>maven-compiler-plugin을 추가할 필요가 없어요.
// build.gradle.kts
group = "org.jetbrains.kotlin"
version = "1.0-SNAPSHOT"
description = "kotlin-junit-complete"
java.sourceCompatibility = JavaVersion.VERSION_17

plugins {
    application
    kotlin("jvm") version "2.4.20"
}

kotlin {
    jvmToolchain(17)
}

application {
    mainClass.set("org.jetbrains.kotlin.junit.App")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.gitlab.klamonte:jexer:1.6.0")

    testImplementation(kotlin("test"))
    testImplementation(libs.org.junit.jupiter.junit.jupiter.api)
    testImplementation(libs.org.junit.jupiter.junit.jupiter.params)
    testRuntimeOnly(libs.org.junit.jupiter.junit.jupiter.engine)
    testRuntimeOnly(libs.org.junit.platform.junit.platform.launcher)
}

tasks.test {
    useJUnitPlatform()
}
  • plugins {} 블록에 kotlin("jvm") 플러그인을 추가해요.
  • JVM 툴체인 버전을 여러분의 Java 버전과 일치하도록 설정해요.
  • dependencies {} 블록에 Kotlin의 테스트 유틸리티를 제공하고 JUnit과 통합되는 kotlin.test 라이브러리를 추가해요.

Kotlin/JVM은 최신 안정 버전인 JUnit 6을 지원해요. 그 버전은 gradle/libs.versions.toml 버전 카탈로그에서 찾을 수 있어요.

평소 버전 카탈로그를 선호한다면, kotlin("jvm") 플러그인도 카탈로그에 추가할 수 있어요.

# gradle/libs.versions.toml
[versions]
kotlin = "2.4.20"
junit = "6.0.3"

[libraries]
org-junit-jupiter-junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
org-junit-jupiter-junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
org-junit-jupiter-junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
org-junit-platform-junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }

[plugins]
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
  • IDE에서 빌드 파일을 다시 불러와요.

빌드 파일 설정에 대한 더 자세한 안내는 프로젝트 구성 문서를 참고하세요.

첫 Kotlin 테스트 추가하기

initial/src/test/javaTodoItemTest.java 테스트는 이미 앱의 기본 동작(항목 생성, 기본값, 고유 ID, 상태 변경)을 검증하고 있어요.

리포지토리 수준 동작을 검증하는 Kotlin 테스트를 추가해서 테스트 커버리지를 확장할 수 있어요.

  • 같은 테스트 소스 디렉터리인 initial/src/test/java로 이동해요.
  • Java 테스트와 같은 패키지에 TodoRepositoryTest.kt 파일을 만들어요.
  • 필드 선언과 설정 함수를 가진 테스트 클래스를 만들어요.
package org.jetbrains.kotlin.junit

import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.DisplayName

internal class TodoRepositoryTest {
    lateinit var repository: TodoRepository
    lateinit var testItem1: TodoItem
    lateinit var testItem2: TodoItem

    @BeforeEach
    fun setUp() {
        repository = TodoRepository()
        testItem1 = TodoItem("Task 1", "Description 1")
        testItem2 = TodoItem("Task 2", "Description 2")
    }
}

JUnit 어노테이션은 Java에서와 똑같이 Kotlin에서도 동작해요.

  • Kotlin의 lateinit 키워드는 나중에 초기화되는 널 아닌 프로퍼티를 선언하게 해 줘요. 덕분에 테스트에서 nullable 타입(TodoRepository?)을 쓰지 않아도 돼요.
  • TodoRepositoryTest 클래스 안에 초기 리포지토리 상태와 그 크기를 확인하는 테스트를 추가해요.
@Test
@DisplayName("Should start with empty repository")
fun shouldStartEmpty() {
    Assertions.assertEquals(0, repository.size())
    Assertions.assertTrue(repository.all.isEmpty())
}

Java의 정적 import와 달리, Jupiter의 Assertions는 클래스로 import되어 assertion 함수의 한정자(qualifier)로 사용돼요.

  • .getAll() 호출 대신, Kotlin에서는 repository.all처럼 Java getter를 프로퍼티로 접근할 수 있어요.
  • 모든 항목의 복사 동작을 검증하는 또 다른 테스트를 작성해요.
@Test
@DisplayName("Should return defensive copy of items")
fun shouldReturnDefensiveCopy() {
    repository.add(testItem1)

    val items1 = repository.all
    val items2 = repository.all

    Assertions.assertNotSame(items1, items2)
    Assertions.assertThrows(
        UnsupportedOperationException::class.java
    ) { items1.clear() }
    Assertions.assertEquals(1, repository.size())
}
  • Kotlin 클래스에서 Java 클래스 객체를 얻으려면 ::class.java를 사용해요.
  • 복잡한 assertion은 특별한 이어쓰기 문자 없이도 여러 줄로 나눌 수 있어요.
  • ID로 항목을 찾는 것을 검증하는 테스트를 추가해요.
@Test
@DisplayName("Should find item by ID")
fun shouldFindItemById() {
    repository.add(testItem1)
    repository.add(testItem2)

     val found = repository.getById(testItem1.id())

     Assertions.assertTrue(found.isPresent)
     Assertions.assertEquals(testItem1, found.get())
}

Kotlin은 Java Optional API와 매끄럽게 동작해요. getter 메서드를 자동으로 프로퍼티로 변환하니까, 여기서 isPresent() 메서드를 프로퍼티처럼 접근하는 거예요.

  • 항목 제거 메커니즘을 검증하는 테스트를 작성해요.
@Test
 @DisplayName("Should remove item by ID")
 fun shouldRemoveItemById() {
     repository.add(testItem1)
     repository.add(testItem2)

     val removed = repository.remove(testItem1.id())

     Assertions.assertTrue(removed)
     Assertions.assertEquals(1, repository.size())
     Assertions.assertTrue(repository.getById(testItem1.id()).isEmpty)
     Assertions.assertTrue(repository.getById(testItem2.id()).isPresent)
 }

 @Test
 @DisplayName("Should return false when removing non-existent item")
 fun shouldReturnFalseForNonExistentRemoval() {
     repository.add(testItem1)

     val removed = repository.remove("non-existent-id")

     Assertions.assertFalse(removed)
     Assertions.assertEquals(1, repository.size())
 }

Kotlin에서는 repository.getById(id).isEmpty처럼 메서드 호출과 프로퍼티 접근을 이어서 쓸 수 있어요.

팁: TodoRepositoryTest 테스트 클래스에 더 많은 기능을 커버하는 테스트를 추가로 붙일 수도 있어요. 샘플 프로젝트의 complete 모듈에서 전체 소스 코드를 볼 수 있어요.

테스트 실행하기

Java와 Kotlin 테스트를 모두 실행해서 프로젝트가 예상대로 동작하는지 확인해 보세요.

  • 거터(gutter) 아이콘으로 테스트를 실행해요. 아니면 initial 디렉터리에서 명령줄로 프로젝트의 모든 테스트를 실행할 수도 있어요.
mvn test
./gradlew test
  • 변수 값 하나를 바꿔서 테스트가 제대로 동작하는지 확인해요. 예를 들어, shouldAddItem 테스트가 잘못된 리포지토리 크기를 기대하도록 수정해 볼게요.
@Test
@DisplayName("Should add item to repository")
fun shouldAddItem() {
    repository.add(testItem1)

    Assertions.assertEquals(2, repository.size())  // Changed from 1 to 2
    Assertions.assertTrue(repository.all.contains(testItem1))
}
  • 테스트를 다시 실행하고, 실패하는지 확인해요.

팁: 테스트가 있는 완전히 구성된 프로젝트는 샘플 프로젝트의 complete 모듈에서 찾을 수 있어요.

다음은 무엇?

Maven으로 Kotlin 프로젝트 테스트하기에 대해 더 알아보세요.