Aplikasi yang berjalan sekali belum tentu benar. Pada Bagian 4 kita mengunci perilaku yang telah dibangun melalui unit test dengan kotlin.test, lalu memaketnya sebagai executable JAR sehingga dapat dijalankan tanpa lingkungan pengembangan. Pengujian dan packaging biasanya dianggap terakhir, tetapi sebenarnya menentukan apakah proyek dapat dipercaya.
Memisahkan Logika yang Dapat Diuji #
Kunci pengujian yang baik: logika bisnis tidak bergantung pada I/O eksternal. RepoTugas di bawah menyimpan daftar tugas di memori sehingga test berjalan cepat dan deterministik, tanpa menyentuh filesystem:
// Bagian 4: Unit test dengan kotlin.test dan packaging sebagai executable JAR.
// Mendemonstrasikan struktur fungsi yang dapat diuji tanpa efek samping,
// pemisahan logika bisnis dari I/O, dan konvensi penamaan test.
data class Tugas(val id: Int, val judul: String, val selesai: Boolean = false)
object RepoTugas {
private var urutanId = 0
private val daftar = mutableListOf<Tugas>()
fun reset() {
urutanId = 0
daftar.clear()
}
fun tambah(judul: String): Tugas {
require(judul.length >= 3) { "Judul minimal 3 karakter" }
urutanId += 1
val tugas = Tugas(urutanId, judul.trim())
daftar.add(tugas)
return tugas
}
fun semua(): List<Tugas> = daftar.toList()
fun tandaiSelesai(id: Int): Tugas? {
val indeks = daftar.indexOfFirst { it.id == id }
if (indeks == -1) return null
val diperbarui = daftar[indeks].copy(selesai = true)
daftar[indeks] = diperbarui
return diperbarui
}
fun hapus(id: Int): Boolean = daftar.removeIf { it.id == id }
}
// Harness test sederhana tanpa framework, mensimulasikan apa yang akan
// ditulis dengan kotlin.test dalam proyek Gradle nyata.
object TestRunner {
private var lulus = 0
private var gagal = 0
fun nama(nama: String, blok: () -> Unit) {
try {
blok()
println("PASS $nama")
lulus++
} catch (e: Throwable) {
println("FAIL $nama: ${e.message}")
gagal++
}
}
fun assertEquals(diharapkan: Any?, aktual: Any?) {
if (diharapkan != aktual) {
throw AssertionError("diharapkan <$diharapkan> aktual <$aktual>")
}
}
fun assertNull(value: Any?) {
if (value != null) throw AssertionError("diharapkan null, aktual <$value>")
}
fun assertTrue(value: Boolean) {
if (!value) throw AssertionError("diharapkan true")
}
fun ringkasan() {
println()
println("Total: ${lulus + gagal} | Lulus: $lulus | Gagal: $gagal")
}
}
fun main() {
RepoTugas.reset()
with(TestRunner) {
nama("tambah menyimpan judul") {
val t = RepoTugas.tambah("Belajar Kotlin")
assertEquals(1, t.id)
assertEquals("Belajar Kotlin", t.judul)
assertTrue(!t.selesai)
}
nama("tambah menolak judul pendek") {
var lempar = false
try {
RepoTugas.tambah("OK")
} catch (e: IllegalArgumentException) {
lempar = true
}
assertTrue(lempar)
}
nama("tandaiSelesai memperbarui status") {
val t = RepoTugas.tandaiSelesai(1)
assertEquals(true, t?.selesai)
}
nama("tandaiSelesai mengembalikan null untuk id tidak ada") {
assertNull(RepoTugas.tandaiSelesai(99))
}
nama("hapus mengembalikan true saat ada") {
assertTrue(RepoTugas.hapus(1))
}
nama("hapus mengembalikan false saat tidak ada") {
assertEquals(false, RepoTugas.hapus(99))
}
ringkasan()
}
}Output yang Dihasilkan:
PASS tambah menyimpan judul
PASS tambah menolak judul pendek
PASS tandaiSelesai memperbarui status
PASS tandaiSelesai mengembalikan null untuk id tidak ada
PASS hapus mengembalikan true saat ada
PASS hapus mengembalikan false saat tidak ada
Total: 6 | Lulus: 6 | Gagal: 0Mari kita bedah komponen utama:
object RepoTugas: repository memori dengan state privat. Fielddaftardisembunyikan; pemanggil hanya dapat berinteraksi melalui fungsi publik.require(judul.length >= 3) { ... }: preconditon yang melemparIllegalArgumentExceptionsaat gagal. Tidak ditangkap karena menandakan bug pemanggilan, bukan kesalahan pengguna.daftar.toList(): mengembalikan snapshot imutabel. Pemanggil tidak dapat mengubah internalRepoTugasmelalui list yang dikembalikan.t.copy(selesai = true):data classmenyediakancopy()untuk membuat instance baru dengan satu field diubah, tanpa mengubah instance asli.TestRunner.nama("...") { ... }: harness sederhana yang menangkap exception dan menandai PASS/FAIL. Pada proyek Gradle nyata, kita memakai@Testdari kotlin.test dengan assertion yang sama.RepoTugas.reset(): dipanggil di awalmain()untuk memastikan state bersih. Pada Gradle,@BeforeEachmelakukan hal serupa.
Struktur Test dengan Gradle #
Pada proyek Gradle nyata, test ditulis dalam src/test/kotlin/ dengan anotasi @Test dari kotlin.test. Berkas build.gradle.kts yang khas terlihat seperti:
plugins {
kotlin("jvm") version "2.0.21"
application
}
dependencies {
testImplementation(kotlin("test"))
}
application {
mainClass.set("TugasKt")
}
tasks.jar {
manifest { attributes("Main-Class" to "TugasKt") }
from(configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) })
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}Konfigurasi di atas memakai plugin application yang menyediakan task run untuk menjalankan aplikasi dan task jar untuk membangun executable JAR. Baris from(...) menggabungkan dependensi runtime ke dalam JAR (fat JAR).
Pengujian Kasus Batas #
Test yang baik tidak hanya memeriksa jalur bahagia. Tiga kategori kasus yang harus dicakup:
- Kasus normal: input valid, output sesuai harapan.
- Kasus batas: ID terbesar, judul tepat pada batas minimal, daftar kosong.
- Kasus galat: input tidak valid, ID tidak ada, file rusak.
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class RepoTugasTest {
@Test
fun `tambah menerima judul tepat batas minimal`() {
RepoTugas.reset()
val t = RepoTugas.tambah("abc")
assertEquals(3, t.judul.length)
}
@Test
fun `tambah menolak judul di bawah batas`() {
RepoTugas.reset()
assertFailsWith<IllegalArgumentException> { RepoTugas.tambah("ab") }
}
}Konvensi penamaan test dengan backtick memungkinkan deskripsi panjang dalam Bahasa Indonesia yang langsung membaca seperti kalimat spesifikasi.
Packaging sebagai Executable JAR #
Packaging menggabungkan semua class yang dikompilasi menjadi satu file JAR dengan manifest yang menyebut titik masuk. Setelah packaging, pengguna cukup menjalankan:
java -jar tugas.jar tambah "Belajar Kotlin"Tidak perlu menginstal Kotlin compiler atau mengunduh dependensi tambahan, karena plugin application menggabungkan semua yang dibutuhkan ke dalam JAR. Distribusi menjadi satu berkas yang dapat dibagikan melalui USB, email, atau repositori internal.
Referensi Lanjutan #
- Kotlin command-line compiler — rujukan kompilator CLI untuk mengompilasi aplikasi menjadi fat JAR yang siap distribusi
- kotlin.test stdlib API
— rujukan
kotlin.testuntuk menulis unit test yang memverifikasi perilaku command-line
Ringkasan Bagian 4 dan Penutup Proyek #
Pengujian dan packaging kini lengkap. RepoTugas dapat diuji tanpa I/O, harness TestRunner menunjukkan pola yang nantinya digantikan oleh kotlin.test, dan konfigurasi Gradle menghasilkan executable JAR yang siap dibagikan.
Proyek Aplikasi Command Line telah selesai. Kita membangun kerangka CLI pada Bagian 1, validasi terstruktur pada Bagian 2, penyimpanan JSON pada Bagian 3, dan pengujian serta packaging pada Bagian 4. Setiap Bagian menghasilkan program yang dapat dijalankan dan diuji sebelum melanjutkan, sehingga kesalahan dapat ditemukan lebih awal. Selanjutnya, kita akan naik ke proyek yang lebih kompleks: Sistem Manajemen Perpustakaan dengan domain modeling dan persistensi database.