Spock dalam build

Menjalankan rangkaian Spock di bawah Gradle dan Maven.

Daftar isi

Spesifikasi Spock memberi nilai penuh ketika alat build proyek mampu menemukan, mengompilasi, dan menjalankannya secara otomatis pada setiap build. Topik ini merangkai konfigurasi tersebut untuk Gradle 8.x dan Maven 3.9.x, mulai dari tata letak sumber, penamaan spesifikasi, dependensi, hingga lokasi laporan hasil uji. Contohnya memakai pustaka pengolah statistik ujian yang menghitung ringkasan nilai peserta, sehingga konfigurasinya dapat disalin ke proyek pustaka sejenis. Setelah membacanya, rangkaian Spock dapat dijalankan dengan perintah gradle test maupun mvn test, dan laporan hasilnya dapat dibuka dari folder proyek.

Tata letak sumber dan penamaan spesifikasi#

Proyek yang diuji dengan Spock memakai dua folder sumber yang terpisah, yaitu src/main/groovy untuk kode utama dan src/test/groovy untuk spesifikasi. Pada Gradle, kedua folder itu disediakan oleh plugin groovy sebagai source set, yakni kelompok sumber yang dikompilasi bersama dengan folder keluaran kelasnya sendiri. Pada Maven, folder yang sama dibaca oleh plugin GMavenPlus yang dikonfigurasikan pada bagian berikutnya. Kode uji diberi paket yang sejajar dengan kode utamanya, misalnya paket statistika, sehingga kelas yang diuji dapat diimpor tanpa pengaturan tambahan.

Spesifikasi Spock mengikuti konvensi penamaan berakhiran Spec, misalnya RataRataSpec untuk menguji kelas RataRata. Konvensi ini bukan sekadar selera karena alat build memakai pola nama untuk memilih kelas uji yang dijalankan. Plugin Surefire, yaitu pelaksana uji bawaan Maven, secara default hanya menjalankan kelas yang cocok dengan pola Test*, *Test, *Tests, atau *TestCase, sehingga kelas berakhiran Spec terlewat bila polanya tidak dinyatakan eksplisit.

Skrip berikut membuat pohon sumber untuk pustaka statistik ujian, lengkap dengan satu kelas utama beserta spesifikasinya, lalu mencetak berkas Groovy yang berhasil dibuat pada kedua folder itu.

groovy struktur-sumber.groovy
// struktur-sumber.groovy: membuat pohon sumber utama dan sumber uji
def tulis(String jalur, String isi) {
    def berkas = new File(jalur)
    berkas.parentFile.mkdirs()
    berkas.text = isi
}

tulis 'src/main/groovy/statistika/RataRata.groovy', '''\
package statistika

class RataRata {
    double hitung(List<Integer> nilai) {
        if (nilai == null || nilai.isEmpty()) return 0.0d
        (nilai.sum() as double) / nilai.size()
    }
}
'''

tulis 'src/test/groovy/statistika/RataRataSpec.groovy', '''\
package statistika

import spock.lang.Specification

class RataRataSpec extends Specification {
    def "rata-rata nilai ujian"() {
        expect:
        new RataRata().hitung(nilai) == harapan

        where:
        nilai        || harapan
        [80, 90]     || 85.0d
        [70, 80, 90] || 80.0d
    }
}
'''

println 'Pohon sumber yang dibaca Gradle dan Maven:'
def berkasGroovy = []
new File('src').eachFileRecurse { if (it.file) berkasGroovy << it.path }
berkasGroovy.sort().each { println it }
text Keluaran
Pohon sumber yang dibaca Gradle dan Maven:
src/main/groovy/statistika/RataRata.groovy
src/test/groovy/statistika/RataRataSpec.groovy

Struktur pada keluaran itulah yang dibaca kedua alat build, dengan kode utama dan kode uji pada pohon yang terpisah serta nama spesifikasi berakhiran Spec. Isi RataRataSpec adalah spesifikasi Spock biasa yang menguji metode hitung dengan tabel data. Dengan berkas contoh tersebut, konfigurasi pada dua bagian berikutnya dapat langsung diuji kebenarannya terhadap spesifikasi nyata.

Konfigurasi Gradle#

Pada Gradle, seluruh pengaturan Spock berkumpul di build.gradle, mulai dari pilihan plugin, repositori, dan dependensi hingga mode pelaksanaan uji. Plugin groovy memperluas plugin Java sehingga task test beserta kedua folder sumber Groovy tersedia tanpa konfigurasi tambahan. Blok dependencies memuat dua pustaka pada konfigurasi testImplementation, yaitu konfigurasi dependensi yang hanya dipakai saat kompilasi dan eksekusi uji.

groovy build.gradle
// tidak-dijalankan
// build.gradle pustaka pengolah statistik ujian dengan Spock
plugins {
    id 'groovy'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.apache.groovy:groovy:5.1.3'
    testImplementation 'org.spockframework:spock-core:2.4-groovy-5.0'
}

test {
    useJUnitPlatform()
}

Deklarasi org.apache.groovy:groovy:5.1.3 menetapkan versi Groovy yang dipakai mengompilasi dan menjalankan kode uji. Deklarasi org.spockframework:spock-core:2.4-groovy-5.0 memuat modul inti Spock, dan sufiks groovy-5.0 pada versinya menandai varian yang dibangun untuk Groovy 5. spock-core membawa salinan Groovy 5.0.3 sebagai dependensi transitifnya, sehingga deklarasi eksplisit di atas diperlukan agar kode utama, kode uji, dan pustakanya bekerja pada satu versi Groovy yang sama.

Baris useJUnitPlatform() pada task test mengalihkan pelaksanaan uji ke mesin JUnit Platform, tempat Spock 2 berjalan. Tanpa baris itu, Gradle memakai dukungan uji lama berbasis JUnit 4, spesifikasi Spock tidak ditemukan, dan laporan uji berisi nol entri meskipun build berhasil. Dengan konfigurasi lengkap, spesifikasi dijalankan dengan perintah gradle test dari folder proyek.

bash
# tidak-dijalankan
# menjalankan seluruh spesifikasi pada proyek Gradle
gradle test

Gradle mengompilasi kedua source set terlebih dahulu, menjalankan setiap spesifikasi melalui mesin JUnit Platform, lalu menulis hasilnya dalam dua bentuk laporan. Berkas HTML yang dapat dibuka di browser berada di build/reports/tests/test/index.html, sedangkan laporan XML per kelas uji berada di build/test-results/test/. Laporan XML inilah yang dibaca sistem CI untuk menampilkan riwayat uji pada setiap build.

Konfigurasi Maven#

Maven memerlukan dua penyiapan sekaligus, yaitu plugin yang mengompilasi Groovy dan plugin yang menjalankan uji. Folder src/test/groovy tidak dikenali Maven secara bawaan, sehingga kompilasinya diserahkan kepada plugin GMavenPlus. Pelaksanaan uji tetap menjadi tugas Surefire, plugin uji bawaan Maven, dengan pola pencarian yang dinyatakan secara eksplisit.

xml pom.xml
<?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>id.tutorial</groupId>
    <artifactId>statistika-ujian</artifactId>
    <version>1.0.0</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>17</maven.compiler.release>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.groovy</groupId>
            <artifactId>groovy</artifactId>
            <version>5.1.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-core</artifactId>
            <version>2.4-groovy-5.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.gmavenplus</groupId>
                <artifactId>gmavenplus-plugin</artifactId>
                <version>5.1.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compileTests</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.6.1</version>
                <configuration>
                    <includes>
                        <include>**/*Spec</include>
                    </includes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Dua dependensi pada pom.xml sama dengan yang dipakai Gradle, hanya saja keduanya diberi scope test sebagai penanda bahwa pustaka hanya dipakai saat kompilasi dan eksekusi uji. Plugin GMavenPlus mendaftarkan dua goal, yaitu unit pekerjaan Maven. Keduanya terikat pada fase bawaannya dalam lifecycle Maven, yakni urutan tahapan build dari kompilasi sampai pengujian. Goal compile berjalan pada fase compile untuk kode utama, sedangkan goal compileTests berjalan pada fase test-compile untuk kode uji. Properti maven.compiler.release juga diikuti GMavenPlus saat memilih kesesuaian bytecode hasil kompilasi Groovy.

Pada Surefire, elemen <includes> wajib diisi karena pola bawaannya hanya mengenali Test*, *Test, *Tests, dan *TestCase. Pola **/*Spec memerintahkan Surefire menjalankan seluruh kelas berakhiran Spec pada folder kelas uji. Surefire 3.x mendeteksi mesin JUnit Platform secara otomatis dari dependensi spock-core, sehingga tidak diperlukan konfigurasi provider tambahan.

Uji dijalankan dengan perintah mvn test. Maven mengeksekusi fase test pada lifecycle-nya, yang mencakup kompilasi kode utama, kompilasi kode uji, dan pelaksanaan Surefire dalam satu urutan. Pada eksekusi pertama, Maven mengunduh dependensi dan plugin yang belum tersedia di repositori lokalnya.

bash
# tidak-dijalankan
# menjalankan seluruh spesifikasi pada proyek Maven
mvn test

Laporan Surefire berada di target/surefire-reports, berupa berkas teks dan satu berkas XML per kelas spesifikasi dengan nama TEST-nama-kelas.xml. Skrip berikut menuliskan contoh ringkas berkas XML itu pada folder yang sama dengan yang dipakai Maven, lalu mencetak isinya agar bentuk laporannya dapat dibaca tanpa menjalankan Maven.

groovy laporan-surefire.groovy
// laporan-surefire.groovy: menuliskan contoh laporan XML Surefire lalu mencetaknya
def isi = '''\
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="statistika.RataRataSpec" time="0.012" tests="2" errors="0" skipped="0" failures="0">
  <testcase name="rata-rata nilai ujian [nilai: [80, 90], harapan: 85.0, #0]" classname="statistika.RataRataSpec" time="0.004"/>
  <testcase name="rata-rata nilai ujian [nilai: [70, 80, 90], harapan: 80.0, #1]" classname="statistika.RataRataSpec" time="0.001"/>
</testsuite>
'''

def folder = new File('target/surefire-reports')
folder.mkdirs()
def laporan = new File(folder, 'TEST-statistika.RataRataSpec.xml')
laporan.text = isi
println laporan.text
text Keluaran
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="statistika.RataRataSpec" time="0.012" tests="2" errors="0" skipped="0" failures="0">
  <testcase name="rata-rata nilai ujian [nilai: [80, 90], harapan: 85.0, #0]" classname="statistika.RataRataSpec" time="0.004"/>
  <testcase name="rata-rata nilai ujian [nilai: [70, 80, 90], harapan: 80.0, #1]" classname="statistika.RataRataSpec" time="0.001"/>
</testsuite>

Elemen <testsuite> merangkum jumlah uji, kegagalan, error, dan yang dilewati, sedangkan setiap elemen <testcase> merekam satu iterasi uji beserta durasinya. Nama uji pada keluaran memuat nilai baris tabel datanya, sesuai perilaku unrolling Spock yang melaporkan setiap baris tabel sebagai uji terpisah. Bentuk serupa juga dihasilkan Gradle pada build/test-results/test/, sehingga kebiasaan membaca laporan XML yang sama berlaku untuk kedua alat build.

Kesalahan umum#

Kesalahan pertama adalah melupakan useJUnitPlatform() pada task test di Gradle. Pelaksana uji lama tidak mengenali spesifikasi Spock 2, sehingga laporan berisi nol uji meskipun status build tetap hijau. Perbaikannya satu baris, yaitu menambahkan useJUnitPlatform() pada blok test, dan kebiasaan pencegahnya adalah memeriksa jumlah uji pada laporan setiap kali konfigurasi uji diubah.

Kesalahan kedua adalah versi Groovy yang tidak seragam antardependensi. Gejalanya muncul saat uji berjalan, misalnya error semacam NoSuchMethodError karena kode yang dikompilasi dengan satu versi Groovy dijalankan di atas versi lain. Jadikan deklarasi org.apache.groovy:groovy pada blok dependensi sebagai satu-satunya rujukan versi Groovy proyek, dan hapus dependensi dengan koordinat lama org.codehaus.groovy milik Groovy 4 dan versi sebelumnya. Deklarasi eksplisit itulah yang menimpa salinan Groovy yang dibawa spock-core secara transitif.

Kesalahan ketiga adalah nama spesifikasi yang tidak cocok dengan pola pencarian. Kelas bernama UjiRataRata atau RataRataPengujian dilewati Surefire tanpa pesan karena pola **/*Spec tidak memuatnya, sehingga laporan hanya berisi kelas yang lolos pola. Ikuti konvensi berakhiran Spec sejak awal, atau tambahkan entri <include> lain secara sadar bila nama lain memang dipertahankan.

Ringkasan#

  • Proyek Spock memisahkan src/main/groovy dan src/test/groovy, dengan spesifikasi berakhiran Spec yang paketnya sejajar dengan kode utamanya.
  • Pada Gradle, plugin groovy menyediakan kedua folder sumber, dua dependensi testImplementation menetapkan Groovy 5.1.3 dan spock-core 2.4-groovy-5.0, useJUnitPlatform() wajib ada pada task test, dan laporannya berada di build/reports/tests/test/index.html serta build/test-results/test/.
  • Pada Maven, GMavenPlus mengompilasi kedua folder Groovy melalui goal compile dan compileTests, Surefire 3.x diarahkan ke pola **/*Spec melalui <includes>, dan laporannya berada di target/surefire-reports.
  • Kelas uji yang tidak cocok pola maupun pelaksana uji yang salah menghasilkan build yang berhasil tanpa uji, sehingga jumlah uji pada laporan selalu diperiksa.

Referensi#