Spock menata pengujian sebagai spesifikasi yang terbaca seperti kalimat, dan topik ini membedah anatomi spesifikasinya dari atas ke bawah. Pembahasan mencakup kelas turunan Specification, metode fitur dengan nama string, blok berlabel given, when, then, expect, cleanup, dan where, pernyataan implisit beserta pesan kegagalannya, serta anotasi deskriptif @Title, @Narrative, dan @Issue. Seluruh contoh berupa skrip Groovy utuh yang mengeksekusi spesifikasinya sendiri melalui peluncur JUnit Platform, sehingga setiap keluaran dapat direproduksi tanpa Gradle maupun Maven.
Spesifikasi dan metode fitur#
Spock adalah framework pengujian untuk Groovy dan Java yang berjalan di atas JUnit Platform, standar eksekusi pengujian pada JVM yang juga menaungi JUnit Jupiter. Sebuah spesifikasi adalah kelas yang mewarisi spock.lang.Specification, dan satu metode di dalamnya menyatakan satu fitur yang diuji. Nama metode fitur boleh berupa string, sehingga dapat ditulis sebagai kalimat utuh seperti def "mencatat dua nilai lalu merangkumnya"(), dan nama itulah yang muncul pada laporan eksekusi. Satu berkas pengujian dengan demikian terbaca sebagai daftar perilaku, bukan daftar nama metode teknis.
Tanpa Gradle maupun Maven, spesifikasi tetap dapat dieksekusi dari skrip Groovy biasa. Anotasi @Grab mengunduh artefak org.spockframework:spock-core:2.4-groovy-5.0 yang cocok untuk Groovy 5. Kelas peluncur (launcher) JUnit Platform sudah tersedia pada distribusi Groovy, sehingga skrip cukup memilih kelas spesifikasi yang diinginkan lalu mengeksekusinya. Sebuah listener anonim (TestExecutionListener) mencatat hasil setiap fitur, dan keluaran dirakit dari catatan itu.
Spesifikasi utuh pertama memakai kelas RekapNilai, penghitung rekap statistik nilai ujian, beserta tiga metode fitur yang mengujinya. Salin berkas berikut, lalu jalankan dengan perintah di bawahnya.
// uji-rekap.groovy: spesifikasi Spock dijalankan langsung dari skrip
@Grab('org.spockframework:spock-core:2.4-groovy-5.0')
import org.junit.platform.engine.TestExecutionResult
import org.junit.platform.engine.discovery.DiscoverySelectors
import org.junit.platform.launcher.TestExecutionListener
import org.junit.platform.launcher.TestIdentifier
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder
import org.junit.platform.launcher.core.LauncherFactory
import spock.lang.Issue
import spock.lang.Narrative
import spock.lang.Specification
import spock.lang.Title
class RekapNilai {
private final daftar = []
RekapNilai(int... nilaiAwal) {
nilaiAwal.each { daftar << it }
}
int getJumlah() { daftar.size() }
double getRataRata() { (daftar.sum() as double) / daftar.size() }
int getTertinggi() { daftar.max() }
void catat(int nilai) { daftar << nilai }
void kosongkan() { daftar.clear() }
}
@Title('Rekap nilai ujian')
@Narrative('Menguji pencatatan nilai dan ringkasan statistiknya.')
class RekapNilaiSpec extends Specification {
def "mencatat dua nilai lalu merangkumnya"() {
given:
def rekap = new RekapNilai()
when:
rekap.catat(80)
rekap.catat(90)
then:
/* ringkasan diperiksa sekaligus lewat with */
with(rekap) {
jumlah == 2
rataRata == 85.0
tertinggi == 90
}
cleanup:
rekap.kosongkan()
}
def "tertinggi diambil dari nilai terbesar yang dicatat"() {
expect:
new RekapNilai(75, 90).tertinggi == 90
}
@Issue('TCK-142')
def "setiap pencatatan menambah jumlah satu"() {
given:
def rekap = new RekapNilai(80)
when:
rekap.catat(95)
then:
rekap.jumlah == old(rekap.jumlah) + 1
}
}
def request = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(RekapNilaiSpec))
.build()
def kontainer = []
def hasil = [:]
def listener = new TestExecutionListener() {
void executionStarted(TestIdentifier id) {
if (!id.isTest()) kontainer << id.displayName
}
void executionFinished(TestIdentifier id, TestExecutionResult hasilEksekusi) {
if (id.isTest()) {
def status = hasilEksekusi.status == TestExecutionResult.Status.SUCCESSFUL ? 'LULUS' : 'GAGAL'
hasil[id.displayName] = status
}
}
}
LauncherFactory.create().execute(request, listener)
println "Kontainer: ${kontainer.join(', ')}"
hasil.each { nama, status -> println "$nama : $status" }
println "Narasi : ${RekapNilaiSpec.getAnnotation(Narrative).value()}"
def metodeIsu = RekapNilaiSpec.declaredMethods.find { it.isAnnotationPresent(Issue) }
println "Isu : ${metodeIsu.getAnnotation(Issue).value().join(', ')}"
println "Total : ${hasil.values().count('LULUS')} lulus, ${hasil.values().count('GAGAL')} gagal"Kontainer: JUnit Jupiter, Spock, Rekap nilai ujian
mencatat dua nilai lalu merangkumnya : LULUS
tertinggi diambil dari nilai terbesar yang dicatat : LULUS
setiap pencatatan menambah jumlah satu : LULUS
Narasi : Menguji pencatatan nilai dan ringkasan statistiknya.
Isu : TCK-142
Total : 3 lulus, 0 gagalBerkas utuh tersebut dijalankan dengan satu perintah.
# tidak-dijalankan
# menjalankan spesifikasi pertama, unduhan pertama butuh jaringan
groovy uji-rekap.groovyBaris pertama keluaran memperlihatkan arsitektur JUnit Platform secara nyata. Peluncur menata eksekusi sebagai pohon kontainer, dan dua mesin pengujian (engine) ikut menanggapi seleksi tersebut, yaitu mesin JUnit Jupiter bawaan distribusi Groovy serta mesin Spock dari @Grab. Mesin Jupiter tidak memiliki satu pun fitur Spock sehingga tidak melaporkan hasil apa pun, sedangkan kelas RekapNilaiSpec tampil di bawah mesin Spock dengan nama Rekap nilai ujian dari anotasi @Title. Tiga metode fitur tercatat sebagai tiga hasil terpisah dengan nama stringnya, dan baris Total merangkum hitungannya.
Blok berlabel dan tugasnya#
Tubuh metode fitur dibagi menjadi blok berlabel, yaitu bagian yang diawali label berakhiran titik dua dan dipisahkan baris kosong. Label menyatakan peran setiap bagian secara eksplisit. Blok given menyiapkan data dan objek, when memanggil aksi yang diuji, then menampung pernyataan harapan, expect menggabungkan aksi dan pernyataan dalam satu blok, sedangkan cleanup merapikan sumber daya setelah fitur selesai, baik lulus maupun gagal. Pada spesifikasi RekapNilaiSpec, fitur pertama memakai given, when, then, dan cleanup sekaligus, sedangkan fitur kedua cukup dengan satu blok expect karena aksinya hanya membaca nilai.
| Blok | Tugas pada fitur |
|---|---|
given | menyiapkan data, objek, dan kondisi awal |
when | menjalankan aksi yang sedang diuji |
then | menampung pernyataan atas hasil aksi |
expect | menggabungkan aksi dan pernyataan dalam satu blok |
cleanup | membersihkan sumber daya setelah fitur selesai |
where | menyediakan tabel data untuk banyak kasus |
Blok where hanya disebutkan sekali di sini karena kajian tabel datanya luas dan layak mendapat pembahasan tersendiri. Bentuknya berpasangan dengan blok expect atau then, dan setiap baris tabelnya menggerakkan satu eksekusi fitur dengan data yang berbeda.
// tidak-dijalankan
def "konversi Celsius ke Fahrenheit untuk beberapa titik"() {
expect:
konverter.keFahrenheit(celsius) == fahrenheit
where:
celsius | fahrenheit
0 | 32
25 | 77
100 | 212
}Komentar juga boleh ditulis di dalam metode fitur. Blok komentar /* */ yang mendahului with pada fitur pertama RekapNilaiSpec menjelaskan langkahnya, dan komentar baris // sah ditempel setelah label. Kerangka berikut merangkum seluruh label beserta keterangan komentarnya.
// tidak-dijalankan
def "kerangka blok"() {
given: // persiapan data dan objek
def rekap = new RekapNilai()
when: // aksi yang sedang diuji
rekap.catat(80)
then: // pernyataan implisit, tanpa kata assert
rekap.jumlah == 1
cleanup: // tetap berjalan meski pernyataan gagal
rekap.kosongkan()
}Pernyataan implisit dan pesan kegagalan#
Pernyataan pada blok then dan expect ditulis tanpa kata assert. Spock mengubah setiap ekspresi boolean di kedua blok tersebut menjadi assertion secara implisit, sehingga rekap.jumlah == 1 pada blok then berarti sama dengan assert rekap.jumlah == 1 pada kode Groovy biasa. Menulis assert secara manual tidak menimbulkan error, tetapi hanya menambah baris yang perannya sudah jelas.
Dua helper bawaan Specification merapikan pernyataan yang lebih panjang. Helper with mengelompokkan pernyataan pada satu objek, sehingga jumlah, rataRata, dan tertinggi pada fitur pertama diperiksa tanpa mengulang nama variabelnya. Helper old menangkap nilai ekspresi sebelum aksi dijalankan, lalu membandingkannya dengan keadaan setelah aksi berjalan, seperti pada fitur ketiga yang memastikan jumlah bertambah satu per pencatatan.
Ketika pernyataan gagal, Spock merender kondisinya bersama nilai setiap bagian yang terlibat. Spesifikasi berikut sengaja memuat harapan yang keliru, yakni fitur yang mengira frasa tiga kata berisi empat kata, agar kegagalan itu ikut tercetak pada keluaran. Skrip menangkap exception pada kegagalan tersebut lalu mencetak isi properti message-nya.
// uji-penghitung.groovy: satu fitur gagal, pesan kegagalannya ditampilkan
@Grab('org.spockframework:spock-core:2.4-groovy-5.0')
import org.junit.platform.engine.TestExecutionResult
import org.junit.platform.engine.discovery.DiscoverySelectors
import org.junit.platform.launcher.TestExecutionListener
import org.junit.platform.launcher.TestIdentifier
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder
import org.junit.platform.launcher.core.LauncherFactory
import spock.lang.Specification
class PenghitungKata {
int hitung(String teks) {
teks.trim().split(/\s+/).length
}
}
class PenghitungKataSpec extends Specification {
def "frasa tiga kata dihitung empat"() {
given:
def penghitung = new PenghitungKata()
def jumlah = penghitung.hitung('groovy itu menyenangkan')
expect:
jumlah == 4
}
}
def request = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(PenghitungKataSpec))
.build()
def hasil = [:]
def pesan = [:]
def listener = new TestExecutionListener() {
void executionFinished(TestIdentifier id, TestExecutionResult hasilEksekusi) {
if (id.isTest()) {
def lulus = hasilEksekusi.status == TestExecutionResult.Status.SUCCESSFUL
hasil[id.displayName] = lulus ? 'LULUS' : 'GAGAL'
if (!lulus) pesan[id.displayName] = hasilEksekusi.throwable.get().message
}
}
}
LauncherFactory.create().execute(request, listener)
hasil.each { nama, status ->
println "$nama : $status"
if (pesan[nama]) {
println ''
println pesan[nama]
}
}
println "Total : ${hasil.values().count('LULUS')} lulus, ${hasil.values().count('GAGAL')} gagal"frasa tiga kata dihitung empat : GAGAL
Condition not satisfied:
jumlah == 4
| |
3 false
Total : 0 lulus, 1 gagalBaris Condition not satisfied menandai pernyataan yang gugur. Baris pertama setelahnya mengulang sumber kondisinya, yaitu jumlah == 4. Baris kedua menandai posisi nilai dengan garis vertikal, dan baris ketiga memuat nilainya, yaitu 3 untuk jumlah dan false untuk seluruh perbandingan. Dari tampilan itu terlihat langsung bahwa implementasi menghitung tiga kata sedangkan spesifikasi menuntut empat, sehingga sisi yang perlu diperbaiki mudah diputuskan. Pesan sedetail ini diperoleh tanpa membaca stack trace sama sekali.
Anotasi deskriptif pada spesifikasi#
Metadata deskriptif menempel pada spesifikasi lewat anotasi dari paket spock.lang. Anotasi @Title mengganti nama spesifikasi pada laporan, dan efeknya sudah terlihat pada baris Kontainer keluaran pertama, yaitu Rekap nilai ujian menggantikan nama kelas RekapNilaiSpec. @Narrative menyimpan keterangan panjang tentang maksud spesifikasi, sedangkan @Issue menautkan satu metode fitur pada nomor isu di pelacak isu proyek. Ketiga anotasi terpasang pada spesifikasi uji-rekap.groovy, dan skrip itu membaca kembali isinya lewat refleksi saat runtime.
Ketiganya tidak mengubah jalannya eksekusi. Perannya baru terasa pada konsumen laporan, misalnya laporan pengujian pada alat build, panel pengujian IDE, atau alat yang menyambungkan hasil uji dengan pelacak isu. Karena nilainya terbaca saat runtime, anotasi ini juga dapat dipakai skrip lain sebagai data, sebagaimana baris Narasi dan Isu pada keluaran pertama menampilkan isi anotasi lewat refleksi. Saat rangkaian pengujian membesar, keterangan semacam ini menjaga maksud spesifikasi tetap terbaca tanpa harus membuka isi setiap fitur.
Kesalahan umum#
Kesalahan pertama adalah menulis metode fitur pada kelas yang belum mewarisi Specification. Label blok tetap terbaca sebagai label Groovy biasa sehingga kelasnya lolos kompilasi, tetapi mesin Spock tidak mengenalinya sebagai spesifikasi. Contoh berikut mengeksekusi kelas semacam itu melalui peluncur yang sama dengan contoh sebelumnya.
// lupa-specification.groovy: kelas fitur tanpa extends Specification
@Grab('org.spockframework:spock-core:2.4-groovy-5.0')
import org.junit.platform.engine.discovery.DiscoverySelectors
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder
import org.junit.platform.launcher.core.LauncherFactory
import org.junit.platform.launcher.listeners.SummaryGeneratingListener
class RekapSalahSpec { // belum mewarisi Specification
def "mencatat dua nilai"() {
given:
def jumlah = 2
when:
def hasil = jumlah * 2
then:
hasil == 4
}
}
def request = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(RekapSalahSpec))
.build()
def listener = new SummaryGeneratingListener()
LauncherFactory.create().execute(request, listener)
println "Fitur ditemukan : ${listener.summary.testsFoundCount}"
println "Lulus ${listener.summary.testsSucceededCount}, gagal ${listener.summary.testsFailedCount}"Fitur ditemukan : 0
Lulus 0, gagal 0Keluarannya menunjukkan nol fitur ditemukan dan tidak ada satu pun pernyataan yang dieksekusi. Laporan semacam ini tampak sehat karena tidak ada kegagalan, padahal tidak ada perilaku yang benar-benar diuji. Deklarasi extends Specification pada kelas spesifikasi karenanya tidak boleh tertinggal, dan jumlah fitur pada ringkasan eksekusi layak diperiksa setiap kali rangkaian dijalankan.
Kesalahan kedua menyangkut konfigurasi dependensi Grape pada skrip spesifikasi.
Ringkasan#
- Spesifikasi Spock adalah kelas yang mewarisi
spock.lang.Specification, dan satu metode fitur dengan nama string menyatakan satu fitur yang dilaporkan terpisah. - Blok
given,when,then,expect, dancleanupmembagi tubuh fitur menurut perannya, sedangkan blokwheremenyediakan tabel data untuk banyak kasus. - Blok
thendanexpectmenegaskan setiap ekspresi boolean tanpa kataassert, dengan helperwithuntuk pernyataan pada satu objek danolduntuk nilai awal fitur, dan kegagalan ditampilkan sebagai Condition not satisfied beserta nilai tiap bagiannya. - Anotasi @Title, @Narrative, dan @Issue melengkapi metadata laporan, sementara kelas tanpa
extends Specificationmenghasilkan nol fitur tanpa kegagalan yang terlihat.
Referensi#
- Spock Primer — Spock Framework — dokumentasi blok berlabel, pernyataan implisit, dan tampilan pesan kegagalan Condition not satisfied.
- Package spock.lang — Spock Framework — javadoc anotasi deskriptif seperti @Title, @Narrative, dan @Issue.
- LauncherFactory — JUnit — API peluncur JUnit Platform yang dipakai skrip contoh untuk mengeksekusi spesifikasi secara langsung.