Subtest dan t.Run

Keluaran pengujian tertata dan pengujian paralel.

Daftar isi

Satu fungsi pengujian yang menguji banyak kasus sering kali sulit didiagnosis karena keluarannya tidak membedakan satu kasus dengan kasus lainnya. Metode t.Run pada paket testing menyelesaikan hal itu dengan membuat subtest, yaitu pengujian bernama yang berada di dalam satu fungsi pengujian, sehingga keluaran go test -v tertata per kasus dan kasus tertentu dapat dijalankan sendirian. Topik ini membahas cara membentuknya, cara memilih subtest yang dijalankan dengan opsi -run, penanganan nama yang berulang, dan penanda t.Parallel untuk menjalankan subtest secara serentak.

Struktur subtest dengan t.Run#

Bentuk paling umum dari subtest adalah perulangan table-driven yang badannya memanggil t.Run alih-alih langsung mengecek hasil. Setiap iterasi menjadi satu subtest dengan nama yang berasal dari tabel, sehingga laporan kegagalannya langsung menunjuk kasusnya tanpa perlu menempelkan nama secara manual pada pesan. Contoh berikut memakai fungsi penghitung kata yang menghitung banyak kata dengan pemisah spasi, tab, dan baris baru:

go kata.go
package main

import (
	"fmt"
	"strings"
)

// HitungKata mengembalikan banyak kata pada sebuah teks. Pemisah yang
// dikenalnya adalah spasi, tab, dan baris baru.
func HitungKata(teks string) int {
	return len(strings.Fields(teks))
}

func main() {
	fmt.Println(HitungKata("kopi susu teh"))
	fmt.Println(HitungKata("  kopi susu  "))
	fmt.Println(HitungKata(""))
}
text Keluaran
3
2
0

Fungsi main memperagakan tiga pemanggilan yang hasilnya berbeda, yaitu tiga kata yang dipisah spasi tunggal, dua kata di antara spasi berlebih, dan teks kosong yang dijawab nol. Ketiga keadaan itu menjadi kasus pada tabel pengujian di bawahnya.

Tabel kasusnya memuat kasus tepi yang penting, yaitu teks kosong, satu kata, pemisah ganda, dan spasi pada tepi teks. Badan perulangannya membungkus pengecekan dalam fungsi tanpa nama yang diteruskan ke t.Run:

go kata_test.go
// tidak-dijalankan: dijalankan melalui go test

package main

import "testing"

func TestHitungKata(t *testing.T) {
	uji := []struct {
		nama  string
		teks  string
		ingin int
	}{
		{"teks kosong", "", 0},
		{"satu kata", "kopi", 1},
		{"spasi ganda", "kopi  susu  teh", 3},
		{"tab dan baris baru", "kopi\tsusu\nteh", 3},
		{"spasi di tepi", "  kopi susu  ", 2},
	}

	for _, u := range uji {
		t.Run(u.nama, func(t *testing.T) {
			if hasil := HitungKata(u.teks); hasil != u.ingin {
				t.Errorf("HitungKata(%q) = %d, seharusnya %d", u.teks, hasil, u.ingin)
			}
		})
	}
}

Baris komentar pertama pada berkas pengujian itu menandai bahwa berkas ini dijalankan melalui go test, bukan secara sendirian. Parameter t pada fungsi tanpa nama itu bukan t milik TestHitungKata, melainkan milik subtest masing-masing, sehingga pelaporannya tercatat atas nama subtest tersebut. Perhatikan pula bahwa nama kasus tidak boleh mengandung karakter yang aneh karena namanya dipakai pada keluaran dan pada pemilihan dengan -run.

Keluaran bertingkat dan pemilihan subtest#

Keluaran rinci dari subtest tersusun bertingkat. Jalankan pengujian utamanya dengan menyaring subtest lain agar keluarannya fokus:

bash
# salin berkas contoh ke folder uji
rm -rf /tmp/gotest-subtest && mkdir -p /tmp/gotest-subtest && cp *.go /tmp/gotest-subtest/ && cd /tmp/gotest-subtest
# buat modul sementara tanpa mencatat pesannya
go mod init contoh >/dev/null 2>&1
# jalankan pengujian utama beserta seluruh subtestnya
go test -v -run TestHitungKata 2>&1 | sed -E 's/^(ok|FAIL)[[:space:]]+contoh[[:space:]].*/\1 contoh/; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/'
text Keluaran
=== RUN   TestHitungKata
=== RUN   TestHitungKata/teks_kosong
=== RUN   TestHitungKata/satu_kata
=== RUN   TestHitungKata/spasi_ganda
=== RUN   TestHitungKata/tab_dan_baris_baru
=== RUN   TestHitungKata/spasi_di_tepi
--- PASS: TestHitungKata (WAKTU)
    --- PASS: TestHitungKata/teks_kosong (WAKTU)
    --- PASS: TestHitungKata/satu_kata (WAKTU)
    --- PASS: TestHitungKata/spasi_ganda (WAKTU)
    --- PASS: TestHitungKata/tab_dan_baris_baru (WAKTU)
    --- PASS: TestHitungKata/spasi_di_tepi (WAKTU)
PASS
ok contoh

Setiap subtest mendapat baris RUN dan PASS sendiri dengan nama lengkapnya, yaitu nama pengujian induk diikuti garis miring dan nama subtestnya. Nama subtest yang berisi spasi diganti garis bawah pada keluaran. Baris PASS milik subtest tampil menjorok di bawah baris induknya, sehingga kedudukannya sebagai bagian dari TestHitungKata terlihat jelas.

Opsi -run menerima ekspresi reguler yang dicocokkan pada nama, dan garis miring pada polanya memisahkan tingkat induk dari tingkat subtest. Pola 'TestHitungKata/spasi' berarti jalankan TestHitungKata, tetapi hanya subtest yang namanya memuat kata spasi:

bash
# jalankan hanya subtest yang namanya memuat kata spasi
go test -v -run 'TestHitungKata/spasi' 2>&1 | sed -E 's/^(ok|FAIL)[[:space:]]+contoh[[:space:]].*/\1 contoh/; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/'
text Keluaran
=== RUN   TestHitungKata
=== RUN   TestHitungKata/spasi_ganda
=== RUN   TestHitungKata/spasi_di_tepi
--- PASS: TestHitungKata (WAKTU)
    --- PASS: TestHitungKata/spasi_ganda (WAKTU)
    --- PASS: TestHitungKata/spasi_di_tepi (WAKTU)
PASS
ok contoh

Hanya dua subtest yang cocok dengan pola itu dijalankan, sedangkan sisanya dilewati sama sekali. Kemampuan ini membuat diagnosis satu kasus gagal menjadi cepat, karena kasus itu dapat diulang sendirian tanpa mengeksekusi tabel secara keseluruhan.

Nama yang diberikan pada t.Run tidak wajib unik. Apabila dua baris tabel diberi nama yang sama, kerangka pengujian tetap menjalankan keduanya dan menambahkan akhiran penomoran #01, #02, dan seterusnya pada nama yang berulang, sehingga setiap subtest tetap memiliki nama lengkap yang unik:

bash
# tambahkan kasus kedua dengan nama yang sama pada tabel
sed -i 's/{"teks kosong", "", 0},/{"teks kosong", "", 0}, {"teks kosong", " ", 0},/' kata_test.go
# jalankan ulang dan amati akhiran penomoran pada nama yang berulang
go test -v -run 'TestHitungKata/teks' 2>&1 | sed -E 's/^(ok|FAIL)[[:space:]]+contoh[[:space:]].*/\1 contoh/; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/'
text Keluaran
=== RUN   TestHitungKata
=== RUN   TestHitungKata/teks_kosong
=== RUN   TestHitungKata/teks_kosong#01
--- PASS: TestHitungKata (WAKTU)
    --- PASS: TestHitungKata/teks_kosong (WAKTU)
    --- PASS: TestHitungKata/teks_kosong#01 (WAKTU)
PASS
ok contoh

Kasus kedua pada tabel di atas memakai teks berisi satu spasi, yang juga dijawab nol kata, sehingga keduanya lolos dengan nama teks_kosong dan teks_kosong#01. Akhiran penomoran itulah yang menjaga pola -run tetap dapat menyasar satu subtest secara persis meskipun namanya berulang. Namun demikian, nama yang deskriptif dan berbeda untuk setiap kasus tetap lebih baik, karena nama itulah yang dibaca pada setiap baris keluaran maupun laporan kegagalan.

Pengujian paralel dengan t.Parallel#

Pemanggilan t.Parallel() di awal badan subtest menandai subtest itu untuk berjalan serentak dengan subtest paralel lainnya. Mekanismenya sebagai berikut: subtest yang ditandai dijeda segera setelah dimulai, seluruh subtest berurutan diselesaikan terlebih dahulu, kemudian subtest yang dijeda dilanjutkan bersama-sama. Pada keluaran -v, penjedaannya tampil sebagai baris === PAUSE dan pelanjutannya sebagai baris === CONT. Urutan pelanjutan antar subtest paralel berubah-ubah antar eksekusi, sehingga bentuk keluaran rinci itu tidak dikutip di sini. Karena itu pula, keluaran yang dikutip pada bagian ini memakai go test tanpa -v:

go paralel_test.go
// tidak-dijalankan: dijalankan melalui go test

package main

import "testing"

func TestParalel(t *testing.T) {
	uji := map[string]int{
		"kopi":             1,
		"kopi susu":        2,
		"kopi susu teh":    3,
		"kopi susu teh es": 4,
	}

	for teks, ingin := range uji {
		t.Run(teks, func(t *testing.T) {
			t.Parallel()
			if hasil := HitungKata(teks); hasil != ingin {
				t.Errorf("HitungKata(%q) = %d, seharusnya %d", teks, hasil, ingin)
			}
		})
	}
}
bash
# jalankan seluruh pengujian tanpa keluaran rinci
go test 2>&1 | sed -E 's/^(ok|FAIL)[[:space:]]+contoh[[:space:]].*/\1 contoh/; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/'
text Keluaran
PASS
ok contoh

Tabelnya sengaja berupa peta, yaitu struktur data yang urutan kunjungannya tidak dijamin, sebab urutan mulai subtest paralel memang tidak dijamin dan tidak perlu dijamin. Status PASS pada keluaran di atas mencakup TestHitungKata dan TestParalel sekaligus, karena go test tanpa -v hanya mencetak ringkasannya.

Ringkasan#

  • t.Run membentuk subtest bernama di dalam satu fungsi pengujian, keluaran -v menampilkannya bertingkat dengan nama lengkap berupa nama induk, garis miring, lalu nama subtestnya, dan nama yang berulang diberi akhiran #01 agar tetap unik.
  • Opsi -run menyaring pengujian dengan ekspresi reguler, dan garis miring pada polanya memisahkan tingkat induk dari tingkat subtest.
  • t.Parallel menandai subtest untuk dijeda lalu dilanjutkan serentak setelah subtest berurutan selesai, dengan baris === PAUSE dan === CONT pada keluaran rinci.
  • Urutan pelanjutan subtest paralel tidak dijamin, sehingga keluaran rincinya berubah-ubah antar eksekusi dan tidak patut dipakai sebagai acuan tetap.

Referensi#