Kelas yang diuji jarang berdiri sendiri, misalnya layanan rekap nilai ujian yang mengarsipkan hasilnya ke penyimpanan luar, dan menguji layanan itu tidak menuntut penyimpanan sungguhan. Spock menyediakan tiruan (test double) yang menggantikan kolaborator semacam itu, yaitu stub yang memberi jawaban, mock yang menuntut interaksi, dan spy yang membungkus objek nyata. Topik ini membahas cara mendeklarasikan ketiganya, menulis interaksi berupa jawaban dan kardinalitas, membaca pesan kegagalan verifikasinya, serta mengenali kapan tiruan justru berlebihan. Seluruh contoh berupa skrip Groovy utuh yang dapat dijalankan langsung, dan keluarannya adalah hasil eksekusi nyata.
Stub, mock, dan spy#
Spock memilah tiruan menurut perannya, dan ketiganya dibuat dengan metode pabrik yang diwarisi dari Specification. Pemanggilan def penyimpan = Mock(Penyimpan) menghasilkan tiruan untuk tipe yang disebutkan, dan tipe pada variabelnya boleh menggantikan parameternya. Stub hanya menyediakan jawaban untuk setiap panggilan tanpa menuntut panggilan itu terjadi. Mock menuntut interaksi yang dideklarasikan benar-benar terjadi, sehingga jumlah pemanggilan yang kurang menggugurkan fitur. Spy membungkus objek nyata dan meneruskan panggilan ke objek aslinya, kecuali satu metode sengaja ditimpa dengan jawaban tiruan.
Ketiganya bersikap longgar (lenient) terhadap panggilan yang tidak dideklarasikan, sehingga panggilan itu tidak menggugurkan fitur. Pada Mock, jawabannya berupa nilai default tipe kembalian metodenya, misalnya null untuk String. Pada Stub, jawabannya lebih membantu, misalnya string kosong untuk String dan list kosong untuk List, sehingga ekspresi seperti penyimpan.ambil(kunci).isEmpty() tetap aman dievaluasi.
Perbedaan ketiganya dirangkum pada tabel berikut.
| Jenis | Jawaban atas panggilan | Verifikasi interaksi |
|---|---|---|
Stub | dari interaksi >>, atau objek kosong bila tidak ada | tidak dilakukan |
Mock | dari interaksi >>, atau nilai default tipe bila tidak ada | dilakukan |
Spy | diteruskan ke objek nyata, kecuali metode ditimpa | dilakukan |
// tidak-dijalankan
def penyimpan = Mock(Penyimpan) // menuntut interaksi terpenuhi
def arsip = Stub(Penyimpan) // hanya menyediakan jawaban
def cadangan = Spy(new PenyimpanMemori()) // membungkus objek nyataStubbing nilai kembalian#
Stubbing adalah penulisan jawaban bagi panggilan yang tidak perlu diverifikasi efeknya, dan interaksinya ditulis dengan operator >>. Bentuk penyimpan.simpan(_) >> 'arsip-42' mengembalikan string itu untuk setiap pemanggilan simpan dengan argumen apa pun, dan jawabannya harus kompatibel dengan tipe kembalian metodenya. Deklarasi interaksi boleh berada di blok setup atau given, tempat yang lazim bagi stub, dan pada blok then jawaban dapat digabung dengan kardinalitas dalam satu baris.
// tidak-dijalankan
penyimpan.simpan(_) >> 'arsip-42' // pada blok setup
1 * penyimpan.simpan(_) >> 'arsip-42' // pada blok then, digabung kardinalitasArgumen yang dicocokkan ditulis segera setelah nama metodenya. Pencocok (matcher) _ menerima argumen apa pun, sedangkan nilai tertentu seperti ambil('arsip-42') hanya mencocokkan panggilan berisi nilai yang sama persis. Ketika beberapa interaksi dapat mencocokkan satu panggilan, interaksi yang dideklarasikan lebih dulu yang menang, sehingga interaksi spesifik perlu ditulis sebelum interaksi _-nya. Satu pengecualian berlaku, yaitu interaksi pada blok then didahulukan atas interaksi yang dideklarasikan di tempat lain, sehingga blok then dapat menimpa deklarasi setup.
Jawaban juga tidak harus berupa satu nilai tetap. Operator >> yang ditulis berantai menyusun jawaban berurutan, dan closure yang menerima daftar argumen dapat menghitung jawabannya dari panggilan yang terjadi. Berkas berikut merangkum keempat bentuk itu dalam satu spesifikasi, dengan layanan RekapNilai yang mengarsipkan rekap statistik nilai ujian melalui antarmuka Penyimpan.
// uji-stub.groovy: Stub menyediakan nilai kembalian tanpa menuntut pemanggilan
@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
// kontrak penyimpanan arsip yang akan digantikan tiruan dalam pengujian
interface Penyimpan {
String simpan(String isi)
String ambil(String kunci)
}
// layanan yang merangkum nilai ujian lalu mengarsipkan rekapnya
class RekapNilai {
private final Penyimpan penyimpan
RekapNilai(Penyimpan penyimpan) {
this.penyimpan = penyimpan
}
String arsipkan(List<Integer> nilai) {
if (nilai.isEmpty()) return null
int jumlah = nilai.size()
double rataRata = (nilai.sum() as double) / jumlah
penyimpan.simpan("jumlah=${jumlah}, rata-rata=${rataRata}")
}
String baca(String kunci) {
penyimpan.ambil(kunci)
}
}
class RekapNilaiSpec extends Specification {
// stub: panggilan tanpa interaksi dijawab dengan nilai default tipe
Penyimpan penyimpan = Stub(Penyimpan)
RekapNilai layanan = new RekapNilai(penyimpan)
def "nilai kembalian tetap dinyatakan pada blok setup"() {
setup:
penyimpan.simpan(_) >> 'arsip-42'
expect:
layanan.arsipkan([80, 90]) == 'arsip-42'
layanan.arsipkan([70]) == 'arsip-42' // dijawab berulang kali
}
def "pencocok argumen membedakan nilai tertentu dan _"() {
setup:
penyimpan.ambil('arsip-42') >> 'jumlah=2, rata-rata=85.0'
penyimpan.ambil(_) >> 'isi generik'
expect:
layanan.baca('arsip-42') == 'jumlah=2, rata-rata=85.0'
layanan.baca('arsip-77') == 'isi generik'
}
def "jawaban berurutan dijawab satu per satu"() {
setup:
penyimpan.simpan(_) >> 'arsip-1' >> 'arsip-2' >> 'arsip-3'
when:
def hasil = [1, 2, 3, 4].collect { layanan.arsipkan([it]) }
then:
hasil == ['arsip-1', 'arsip-2', 'arsip-3', 'arsip-3']
}
def "closure menghitung jawaban dari argumen panggilan"() {
setup:
penyimpan.simpan(_) >> { args -> "ARSIP-${args[0].length()} karakter" }
expect:
layanan.arsipkan([80, 90]) == 'ARSIP-24 karakter'
}
}
// menjalankan seluruh spesifikasi lewat launcher JUnit Platform
def jalankan(Class spec) {
def launcher = LauncherFactory.create()
def permintaan = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(spec))
.build()
launcher.execute(permintaan, new TestExecutionListener() {
void executionFinished(TestIdentifier id, TestExecutionResult hasil) {
if (!id.isTest() || id.isContainer()) return
String status = hasil.status == TestExecutionResult.Status.SUCCESSFUL ? 'LULUS' : 'GAGAL'
println "${status} ${id.displayName}"
}
})
}
jalankan(RekapNilaiSpec)LULUS nilai kembalian tetap dinyatakan pada blok setup
LULUS pencocok argumen membedakan nilai tertentu dan _
LULUS jawaban berurutan dijawab satu per satu
LULUS closure menghitung jawaban dari argumen panggilanFitur pertama mendeklarasikan jawaban tetap pada blok setup, lalu panggilan berikutnya dijawab dengan nilai yang sama tanpa batas jumlah. Fitur kedua membandingkan pencocok spesifik dengan pencocok _, dan jawaban spesifik menang karena dideklarasikan lebih dulu. Fitur ketiga merangkai jawaban berurutan, dan panggilan keempatnya mengulang jawaban terakhir. Fitur keempat memakai closure yang menerima daftar argumen panggilan, sehingga args[0] berisi teksnya dan jawaban dihitung dari panjangnya.
Setiap berkas contoh mengeksekusi spesifikasinya sendiri lewat launcher JUnit Platform yang hadir sebagai dependensi transitif @Grab berkoordinat org.spockframework:spock-core:2.4-groovy-5.0, sehingga dijalankan langsung dengan perintah groovy.
# tidak-dijalankan
# menjalankan berkas contoh dari terminal
groovy uji-stub.groovyKardinalitas interaksi#
Kardinalitas menyatakan berapa kali sebuah interaksi harus terjadi, ditulis dengan angka diikuti * sebelum pemanggilan metodenya, misalnya 1 * penyimpan.simpan(_). Angka tunggal berarti tepat sekali, rentang ditulis di dalam kurung seperti (1..3) *, dan (0.._) * berarti nol kali hingga tak terbatas. Interaksi berkardinalitas hanya dimiliki tiruan yang memverifikasi, yaitu Mock dan Spy, sedangkan Stub menolaknya. Verifikasi berlangsung setelah blok when selesai, sehingga interaksi blok then dicek saat itu, sedangkan interaksi blok given atau setup dicek pada akhir fitur.
Berkas berikut menguji layanan yang sama dengan penyimpan berupa Mock.
// uji-mock.groovy: Mock memverifikasi interaksi beserta kardinalitasnya
@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
interface Penyimpan {
String simpan(String isi)
String ambil(String kunci)
}
class RekapNilai {
private final Penyimpan penyimpan
RekapNilai(Penyimpan penyimpan) {
this.penyimpan = penyimpan
}
String arsipkan(List<Integer> nilai) {
if (nilai.isEmpty()) return null
int jumlah = nilai.size()
double rataRata = (nilai.sum() as double) / jumlah
penyimpan.simpan("jumlah=${jumlah}, rata-rata=${rataRata}")
}
String baca(String kunci) {
penyimpan.ambil(kunci)
}
}
class RekapNilaiSpec extends Specification {
// mock: setiap interaksi berkardinalitas diverifikasi pada akhir fitur
Penyimpan penyimpan = Mock(Penyimpan)
RekapNilai layanan = new RekapNilai(penyimpan)
def "kardinalitas satu dan stubbing digabung dalam satu interaksi"() {
when:
String hasil = layanan.arsipkan([80, 90])
then:
1 * penyimpan.simpan(_) >> 'arsip-42'
hasil == 'arsip-42'
}
def "kardinalitas berupa rentang dan tak terbatas"() {
when:
layanan.arsipkan([80, 90])
layanan.arsipkan([70, 75])
then:
(1..3) * penyimpan.simpan(_)
(0.._) * penyimpan.ambil(_)
}
def "interaksi pada blok given dicek pada akhir fitur"() {
given:
1 * penyimpan.simpan(_) >> 'arsip-42'
when:
String hasil = layanan.arsipkan([80, 90])
then:
hasil == 'arsip-42'
}
def "pembatasan isi argumen dengan closure matcher"() {
when:
layanan.arsipkan([80, 90])
then:
1 * penyimpan.simpan({ it.startsWith('jumlah=2') }) >> 'arsip-42'
}
}
// menjalankan seluruh spesifikasi lewat launcher JUnit Platform
def jalankan(Class spec) {
def launcher = LauncherFactory.create()
def permintaan = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(spec))
.build()
launcher.execute(permintaan, new TestExecutionListener() {
void executionFinished(TestIdentifier id, TestExecutionResult hasil) {
if (!id.isTest() || id.isContainer()) return
String status = hasil.status == TestExecutionResult.Status.SUCCESSFUL ? 'LULUS' : 'GAGAL'
println "${status} ${id.displayName}"
}
})
}
jalankan(RekapNilaiSpec)LULUS kardinalitas satu dan stubbing digabung dalam satu interaksi
LULUS kardinalitas berupa rentang dan tak terbatas
LULUS interaksi pada blok given dicek pada akhir fitur
LULUS pembatasan isi argumen dengan closure matcherFitur pertama menggabungkan kardinalitas dan jawaban dalam satu interaksi, sehingga satu baris sekaligus menuntut satu kali pemanggilan simpan dan menentukan jawabannya. Fitur kedua memakai rentang (1..3) * yang terpenuhi oleh dua kali pemanggilan yang terjadi, ditambah (0.._) * yang puas meskipun metodenya sama sekali tidak dipanggil. Fitur ketiga memindahkan interaksinya ke blok given, dan hasilnya tetap terverifikasi karena interaksi semacam itu dicek pada akhir fitur. Fitur keempat mempersempit pencocok argumen dengan closure { it.startsWith('jumlah=2') }, sehingga hanya argumen yang diawali jumlah=2 yang dihitung cocok.
Kontras dengan Stub kini utuh. Stub tidak pernah menggugurkan fitur karena jumlah pemanggilannya, sedangkan Mock justru ditulis untuk menuntut jumlah dan bentuk interaksi sebagai bagian dari perilaku yang diuji.
Spy pada objek nyata#
Spy adalah tiruan yang berdiri di atas objek nyata, sehingga panggilan yang tidak ditimpa tetap dijawab oleh kode aslinya. Objek dasarnya wajib berupa kelas, bukan antarmuka, dan bila argumen konstruktor tidak diberikan, konstruktor tanpa argumen yang dipakai. Karena panggilan nyata tetap berjalan, perilaku lama dapat diverifikasi sekaligus metode tertentu ditimpa. Berkas berikut memperagakan Spy atas PenyimpanMemori, yaitu penyimpanan arsip sederhana yang menyimpan pasangan kunci dan isinya di memori.
// uji-spy.groovy: Spy membungkus objek nyata lalu meneruskan panggilannya
@Grab('org.spockframework:spock-core:2.4-groovy-5.0')
@Grab('net.bytebuddy:byte-buddy:1.14.9')
@Grab('org.objenesis:objenesis:3.3')
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
interface Penyimpan {
String simpan(String isi)
String ambil(String kunci)
}
// implementasi nyata: arsip tersimpan dalam map di memori
class PenyimpanMemori implements Penyimpan {
private final simpanan = [:]
String simpan(String isi) {
String kunci = "arsip-${simpanan.size() + 1}"
simpanan[kunci] = isi
kunci
}
String ambil(String kunci) {
simpanan[kunci]
}
}
class RekapNilai {
private final Penyimpan penyimpan
RekapNilai(Penyimpan penyimpan) {
this.penyimpan = penyimpan
}
String arsipkan(List<Integer> nilai) {
if (nilai.isEmpty()) return null
int jumlah = nilai.size()
double rataRata = (nilai.sum() as double) / jumlah
penyimpan.simpan("jumlah=${jumlah}, rata-rata=${rataRata}")
}
String baca(String kunci) {
penyimpan.ambil(kunci)
}
}
class RekapNilaiSpec extends Specification {
def "spy meneruskan panggilan ke objek nyata dan tetap terverifikasi"() {
given:
def penyimpan = Spy(PenyimpanMemori)
def layanan = new RekapNilai(penyimpan)
when:
String kunci = layanan.arsipkan([80, 90])
then:
kunci == 'arsip-1'
layanan.baca(kunci) == 'jumlah=2, rata-rata=85.0'
1 * penyimpan.simpan(_)
}
def "spy dapat menimpa satu metode saja"() {
given:
def penyimpan = Spy(PenyimpanMemori)
penyimpan.ambil('arsip-hilang') >> 'isi cadangan'
when:
def gantian = penyimpan.ambil('arsip-hilang')
def tersimpan = penyimpan.simpan('uji')
def bacaan = penyimpan.ambil('arsip-1')
then:
gantian == 'isi cadangan'
tersimpan == 'arsip-1'
bacaan == 'uji'
}
}
// menjalankan seluruh spesifikasi lewat launcher JUnit Platform
def jalankan(Class spec) {
def launcher = LauncherFactory.create()
def permintaan = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(spec))
.build()
launcher.execute(permintaan, new TestExecutionListener() {
void executionFinished(TestIdentifier id, TestExecutionResult hasil) {
if (!id.isTest() || id.isContainer()) return
String status = hasil.status == TestExecutionResult.Status.SUCCESSFUL ? 'LULUS' : 'GAGAL'
println "${status} ${id.displayName}"
}
})
}
jalankan(RekapNilaiSpec)LULUS spy meneruskan panggilan ke objek nyata dan tetap terverifikasi
LULUS spy dapat menimpa satu metode sajaFitur pertama membiarkan seluruh panggilan diteruskan, sehingga rekap benar-benar tersimpan dan dapat dibaca kembali, sementara interaksi 1 * penyimpan.simpan(_) tetap memverifikasi jumlah pemanggilannya. Fitur kedua menimpa satu metode saja, yaitu ambil('arsip-hilang') yang dijawab ‘isi cadangan’, sedangkan metode lainnya tetap menjalankan kode aslinya. Perilaku ganda semacam ini berguna ketika sebagian perilaku kelas nyata mahal, misalnya akses jaringan, sedangkan sisanya murah dan layak dijalankan sungguhan. Kedua fitur memakai layanan RekapNilai yang sama dengan contoh sebelumnya, sehingga ketiga jenis tiruan dapat dibandingkan pada satu skenario arsip.
Pesan kegagalan verifikasi#
Interaksi berkardinalitas yang tidak terpenuhi menggugurkan fitur dengan pesan error yang merinci kekurangannya. Berkas berikut sengaja memuat kekeliruan logika, yaitu fitur yang menuntut satu kali pemanggilan simpan, padahal daftar nilai yang kosong membuat arsipkan kembali lebih awal tanpa menyentuh penyimpanan. Tiruannya berupa Mock, karena Stub tidak akan menuntut apa pun dan fitur sejenis justru lulus.
// uji-gagal.groovy: verifikasi interaksi yang gagal beserta pesannya
@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
interface Penyimpan {
String simpan(String isi)
String ambil(String kunci)
}
class RekapNilai {
private final Penyimpan penyimpan
RekapNilai(Penyimpan penyimpan) {
this.penyimpan = penyimpan
}
String arsipkan(List<Integer> nilai) {
if (nilai.isEmpty()) return null
int jumlah = nilai.size()
double rataRata = (nilai.sum() as double) / jumlah
penyimpan.simpan("jumlah=${jumlah}, rata-rata=${rataRata}")
}
}
// fitur menuntut satu kali simpan(), tetapi daftar kosong membuat
// arsipkan() kembali lebih awal sehingga penyimpan tidak pernah dipanggil
class RekapKosongSpec extends Specification {
Penyimpan penyimpan = Mock(Penyimpan)
RekapNilai layanan = new RekapNilai(penyimpan)
def "arsipan kosong tetap menyimpan rekap"() {
when:
layanan.arsipkan([])
then:
1 * penyimpan.simpan(_)
}
}
// menjalankan seluruh spesifikasi lewat launcher JUnit Platform
def jalankan(Class spec) {
def launcher = LauncherFactory.create()
def permintaan = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(spec))
.build()
launcher.execute(permintaan, new TestExecutionListener() {
void executionFinished(TestIdentifier id, TestExecutionResult hasil) {
if (!id.isTest() || id.isContainer()) return
String status = hasil.status == TestExecutionResult.Status.SUCCESSFUL ? 'LULUS' : 'GAGAL'
println "${status} ${id.displayName}"
if (status == 'GAGAL') println hasil.throwable.get().message
}
})
}
jalankan(RekapKosongSpec)GAGAL arsipan kosong tetap menyimpan rekap
Too few invocations for:
1 * penyimpan.simpan(_) (0 invocations)
Unmatched invocations (ordered by similarity):
NonePesan Too few invocations menyatakan jumlah pemanggilan lebih sedikit daripada tuntutan, lengkap dengan interaksinya dan hitungannya. Bagian Unmatched invocations mendaftar panggilan yang terjadi tetapi tidak cocok dengan interaksi mana pun, dan daftar itu berisi None karena tidak ada satu pun panggilan yang terjadi. Dari bentuk pesan itu terlihat bahwa kesalahannya ada pada alur kode, bukan pada isi argumennya. Bila panggilan memang terjadi dengan argumen berbeda, daftar yang sama menampilkannya berurutan menurut kemiripannya dengan interaksi yang dituntut, sehingga perbedaan argumen langsung terbaca.
Kapan tiruan berlebihan#
Tiruan yang dipasang pada semua kolaborator sering menipu pembuatnya. Uji yang menuntut 1 * penyimpan.simpan({ it.startsWith('jumlah=2') }) sebenarnya memeriksa detail implementasi, yaitu cara layanan merangkai teks rekapnya, bukan perilaku yang dijanjikannya. Begitu format teks itu disunting dengan sah, pengujian ikut gugur padahal perilakunya masih benar, sehingga biaya perawatannya menumpuk. Tiruan paling tepat dipakai pada batas sistem yang mahal atau tidak pasti, misalnya penyimpanan eksternal atau jaringan, sedangkan logika murni seperti perhitungan rata-rata cukup diuji dengan nilai nyata tanpa tiruan.
Pertimbangan yang sama berlaku saat memilih antara Stub dan Mock, yaitu menanyakan apakah jumlah pemanggilan bagian dari perilaku yang diuji atau sekadar efek samping yang boleh diabaikan. Objek nyata yang ringkas pun lebih baik daripada tiruannya, seperti PenyimpanMemori yang sesederhana pasangan kunci dan isi di memori. Sikap longgar Spock ikut menahan kecenderungan ini, karena panggilan yang tidak dideklarasikan tidak menggugurkan fitur, sehingga setiap interaksi yang ditulis memang perlu dipertanggungjawabkan.
Kesalahan umum#
Kesalahan pertama adalah menaruh kardinalitas pada Stub. Stub memang tidak memverifikasi interaksi, sehingga tuntutan seperti 1 * penyimpan.simpan(_) bertentangan dengan perannya. Spock menolaknya dengan InvalidSpecException pada saat fitur dieksekusi, dan pesannya sekaligus menyarankan dua perbaikan.
// kesalahan-tiruan.groovy: kardinalitas pada Stub ditolak oleh Spock
@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
interface Penyimpan {
String simpan(String isi)
}
// stub tidak memverifikasi interaksi, sehingga kardinalitasnya ditolak
class KardinalitasStubSpec extends Specification {
def penyimpan = Stub(Penyimpan)
def "kardinalitas pada stub"() {
when:
penyimpan.simpan('isi rekap')
then:
1 * penyimpan.simpan(_)
}
}
// menjalankan seluruh spesifikasi lewat launcher JUnit Platform
def jalankan(Class spec) {
def launcher = LauncherFactory.create()
def permintaan = LauncherDiscoveryRequestBuilder.request()
.selectors(DiscoverySelectors.selectClass(spec))
.build()
launcher.execute(permintaan, new TestExecutionListener() {
void executionFinished(TestIdentifier id, TestExecutionResult hasil) {
if (!id.isTest() || id.isContainer()) return
String status = hasil.status == TestExecutionResult.Status.SUCCESSFUL ? 'LULUS' : 'GAGAL'
println "${status} ${id.displayName}"
if (status == 'GAGAL') {
def t = hasil.throwable.get()
println "JENIS: ${t.getClass().name}"
println t.message
}
}
})
}
jalankan(KardinalitasStubSpec)GAGAL kardinalitas pada stub
JENIS: org.spockframework.runtime.InvalidSpecException
Stub 'penyimpan' matches the following required interaction:
1 * penyimpan.simpan(_) (0 invocations)
Remove the cardinality (e.g. '1 *'), or turn the stub into a mock.Pesan InvalidSpecException menyebut nama Stub-nya, interaksi yang bermasalah, dan dua pilihan perbaikan yang mengikuti maksud pengujian. Kardinalitas dihapus bila yang penting hanya jawabannya, sedangkan tiruan diubah menjadi Mock bila jumlah pemanggilannya harus dituntut.
Kesalahan kedua terjadi saat Spy dibuat atas kelas nyata. Tiruan untuk kelas konkret dibangun lewat proxy dari pustaka Byte Buddy, sedangkan pembuatan objeknya dibantu pustaka Objenesis, dan keduanya tidak dibawa oleh dependensi spock-core.
Ringkasan#
- Tiruan dibuat dengan metode pabrik dari
Specification, yaitu Stub yang hanya menyediakan jawaban, Mock yang menuntut interaksinya terpenuhi, dan Spy yang membungkus objek nyata. - Jawaban ditulis dengan
>>pada bloksetup,given, atauthen, boleh berupa nilai tetap, jawaban berurutan yang mengulang nilai terakhir, atau closure yang menerima daftar argumen, dan interaksi yang dideklarasikan lebih dulu menang atas pencocok_. - Kardinalitas
1 *,(1..3) *, dan(0.._) *hanya sah pada Mock dan Spy, dengan interaksi blokthendicek setelahwhendan interaksi blokgivendicek pada akhir fitur. - Kegagalan verifikasi memunculkan Too few invocations beserta daftar Unmatched invocations, kardinalitas pada Stub ditolak dengan InvalidSpecException, dan Spy atas kelas nyata membutuhkan Byte Buddy serta Objenesis.
Referensi#
- Interaction Based Testing — Spock Framework — dokumentasi resmi mocking, stubbing, spying, kardinalitas, dan pesan kegagalan verifikasinya.
- Spock Primer — Spock Framework — bab pengantar Spock yang memperkenalkan blok berlabel dan interaksinya.
- LauncherFactory — JUnit Team — API peluncur JUnit Platform yang dipakai skrip contoh untuk mengeksekusi spesifikasi.