Saat seluruh test berstatus PASS, pertanyaan berikutnya muncul: seberapa banyak baris kode produksi yang benar-benar pernah dieksekusi oleh test kita? Coverage menjawabnya dengan menginstrumentasi kode saat kompilasi, lalu menghitung statement mana yang terlewati. Go menyediakannya langsung di go test lewat flag -cover. Pada chapter ini kita akan mengukur coverage sebuah package kecil, membaca laporan per fungsi dari go tool cover, lalu menafsirkan angkanya secara sehat.
Konsep Coverage #
Saat flag -cover aktif, compiler menyisipkan penghitung pada setiap basic block kode sebelum test dijalankan. Setelah test selesai, go test membandingkan jumlah statement yang tercatat dengan total statement package, lalu melaporkan persentasenya pada baris ok. Karena murni menghitung statement pada kode yang sama, persentasenya stabil — pengukuran berulang menghasilkan angka yang sama.
Ada dua tingkatan laporan: ringkasan satu angka dari go test -cover, dan rincian per fungsi dari go tool cover. Rincian inilah yang paling berguna karena angka rata-rata menyembunyikan fungsi yang luput dari test.
Kode dan Test yang Akan Diukur #
Package uji kita berisi tiga fungsi domain toko daring plus main, simpan sebagai diskon.go:
package main
import "fmt"
// HitungDiskon menghitung nominal diskon dari harga dan tingkat member:
// "gold" mendapat 20%, "silver" 10%, tingkat lain tanpa diskon.
func HitungDiskon(harga int, tingkat string) int {
switch tingkat {
case "gold":
return harga * 20 / 100
case "silver":
return harga * 10 / 100
default:
return 0
}
}
// KategoriPembeli menentukan label pembeli dari total belanja tahunan.
func KategoriPembeli(total int) string {
if total < 0 {
return "tidak valid"
}
if total >= 10_000_000 {
return "platinum"
}
if total >= 1_000_000 {
return "gold"
}
return "reguler"
}
// KuponValid memeriksa kecocokan kode kupon dengan daftar kupon aktif.
func KuponValid(kode string) bool {
if kode == "" {
return false
}
for _, k := range []string{"HEMAT10", "GOSPORT", "NEWYEAR"} {
if kode == k {
return true
}
}
return false
}
func main() {
fmt.Println(HitungDiskon(150000, "gold"))
fmt.Println(KategoriPembeli(2_500_000))
fmt.Println(KuponValid("HEMAT10"))
}Jalankan berkas tersebut dari folder chapter:
go run diskon.goOutput yang Dihasilkan:
30000
gold
trueMari kita telaah bagian-bagian kuncinya:
- Baris 7-16 (
func HitungDiskon): satuswitchdengan tiga cabang — masing-masing cabang adalah statement terpisah yang hanya terhitung tercakup bila test mengirim tingkat member yang sesuai. - Baris 19-30 (
func KategoriPembeli): empat cabangreturn; cabangtidak validdanplatinumsengaja belum punya test agar muncul sebagai celah coverage. - Baris 33-43 (
func KuponValid): dua cabang awal kosong/tidak valid dan satu loop pencarian kupon. - Baris 45-49 (
func main): tiga pencetakan sederhana — nanti terlihat bahwago testtidak pernah menjalankanmain, sehingga fungsi ini tampil 0%.
Test-nya berada pada diskon_test.go:
package main
import "testing"
func TestHitungDiskon(t *testing.T) {
uji := []struct {
harga int
tingkat string
diharapkan int
}{
{150000, "gold", 30000},
{150000, "silver", 15000},
{150000, "bronze", 0},
}
for _, u := range uji {
if got := HitungDiskon(u.harga, u.tingkat); got != u.diharapkan {
t.Errorf("HitungDiskon(%d, %q) = %d, diharapkan %d", u.harga, u.tingkat, got, u.diharapkan)
}
}
}
func TestKategoriPembeli(t *testing.T) {
if got := KategoriPembeli(2_500_000); got != "gold" {
t.Errorf("KategoriPembeli(2500000) = %q, diharapkan %q", got, "gold")
}
if got := KategoriPembeli(250_000); got != "reguler" {
t.Errorf("KategoriPembeli(250000) = %q, diharapkan %q", got, "reguler")
}
}
func TestKuponValid(t *testing.T) {
uji := []struct {
kode string
diharapkan bool
}{
{"HEMAT10", true},
{"GOSPORT", true},
{"", false},
{"EXPIRED", false},
}
for _, u := range uji {
if got := KuponValid(u.kode); got != u.diharapkan {
t.Errorf("KuponValid(%q) = %v, diharapkan %v", u.kode, got, u.diharapkan)
}
}
}Jalankan dari folder chapter:
go test -v ./...Output yang Dihasilkan:
=== RUN TestHitungDiskon
--- PASS: TestHitungDiskon (0.00s)
=== RUN TestKategoriPembeli
--- PASS: TestKategoriPembeli (0.00s)
=== RUN TestKuponValid
--- PASS: TestKuponValid (0.00s)
PASS
ok tutorial-website/content/go/testing/coverage 0.002sMengukur dengan go test -cover #
Tambahkan flag -cover untuk melihat angka cakupan:
go test -cover ./...Output yang Dihasilkan:
ok tutorial-website/content/go/testing/coverage 0.002s coverage: 75.0% of statementsAngka 75.0% berarti tiga perempat statement package pernah dieksekusi test — terasa tinggi, padahal ada cabang yang belum diuji. Di sinilah perlunya laporan rinci.
Coverprofile dan Analisis Per Fungsi #
Simpan hasil pengukuran ke berkas profil, lalu baca ringkasannya per fungsi:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.outPerintah pertama mencetak baris ok seperti bagian sebelumnya dan sekaligus menulis berkas coverage.out di folder chapter. Perintah kedua membaca berkas tersebut dan mencetak laporan berikut:
Output yang Dihasilkan:
tutorial-website/content/go/testing/coverage/diskon.go:7: HitungDiskon 100.0%
tutorial-website/content/go/testing/coverage/diskon.go:19: KategoriPembeli 71.4%
tutorial-website/content/go/testing/coverage/diskon.go:33: KuponValid 100.0%
tutorial-website/content/go/testing/coverage/diskon.go:45: main 0.0%
total: (statements) 75.0%Laporan tersebut membuka letak celahnya: KategoriPembeli hanya 71.4% karena test tidak mengirim nilai negatif maupun di atas 10 juta; main 0.0% karena go test tidak menjalankan fungsi utama. Menambah dua kasus — KategoriPembeli(12_000_000) dan KategoriPembeli(-1) — menaikkan KategoriPembeli ke 100.0% dan total ke 85.0%; sisanya main yang bukan target test.
Berkas coverage.out sendiri berisi daftar blok kode beserta penghitungnya, dengan beberapa baris pertamanya seperti berikut:
mode: set
tutorial-website/content/go/testing/coverage/diskon.go:8.2,8.17 1 1
tutorial-website/content/go/testing/coverage/diskon.go:10.3,10.26 1 1
tutorial-website/content/go/testing/coverage/diskon.go:12.3,12.26 1 1
tutorial-website/content/go/testing/coverage/diskon.go:14.3,14.11 1 1
tutorial-website/content/go/testing/coverage/diskon.go:20.2,20.15 1 1Setiap baris memuat alamat blok berkas:baris.kolom,baris.kolom, jumlah statement, dan hitungan eksekusi blok. Nilai 0 pada kolom terakhir itulah penanda cabang yang tidak pernah terlewati. Berkas ini sebaiknya tidak dikomit ke repositori karena dapat dibuat ulang kapan saja.
Menafsirkan Angka Coverage #
Coverage mengukur kode yang dieksekusi, bukan kebenaran perilaku. Test tanpa assertion akan menaikkan coverage tinggi tanpa membuktikan apa pun, dan cabang yang dieksekusi tetap bisa gagal diverifikasi bila assertion-nya lemah.
Angka yang baik dipakai secara komparatif: bandingkan coverage sebelum dan sesudah menulis test baru, awasi fungsi yang persentasenya turun karena menambah cabang tanpa menambah test, dan jadikan laporan per fungsi sebagai daftar kerja. Setelah kebenaran fungsi tunggal terukur, kita beranjak ke kelas bug yang lebih senyap: dua goroutine yang menulis variabel yang sama pada chapter race detector.
Referensi #
- Blog: The coverage story — desain alat coverage bawaan Go.
- Testing flags pada perintah go
— flag
-coverdan-coverprofile.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
diskon_test.go
Unduh berkaspackage main
import "testing"
func TestHitungDiskon(t *testing.T) {
uji := []struct {
harga int
tingkat string
diharapkan int
}{
{150000, "gold", 30000},
{150000, "silver", 15000},
{150000, "bronze", 0},
}
for _, u := range uji {
if got := HitungDiskon(u.harga, u.tingkat); got != u.diharapkan {
t.Errorf("HitungDiskon(%d, %q) = %d, diharapkan %d", u.harga, u.tingkat, got, u.diharapkan)
}
}
}
func TestKategoriPembeli(t *testing.T) {
if got := KategoriPembeli(2_500_000); got != "gold" {
t.Errorf("KategoriPembeli(2500000) = %q, diharapkan %q", got, "gold")
}
if got := KategoriPembeli(250_000); got != "reguler" {
t.Errorf("KategoriPembeli(250000) = %q, diharapkan %q", got, "reguler")
}
}
func TestKuponValid(t *testing.T) {
uji := []struct {
kode string
diharapkan bool
}{
{"HEMAT10", true},
{"GOSPORT", true},
{"", false},
{"EXPIRED", false},
}
for _, u := range uji {
if got := KuponValid(u.kode); got != u.diharapkan {
t.Errorf("KuponValid(%q) = %v, diharapkan %v", u.kode, got, u.diharapkan)
}
}
}
diskon.go
Unduh berkaspackage main
import "fmt"
// HitungDiskon menghitung nominal diskon dari harga dan tingkat member:
// "gold" mendapat 20%, "silver" 10%, tingkat lain tanpa diskon.
func HitungDiskon(harga int, tingkat string) int {
switch tingkat {
case "gold":
return harga * 20 / 100
case "silver":
return harga * 10 / 100
default:
return 0
}
}
// KategoriPembeli menentukan label pembeli dari total belanja tahunan.
func KategoriPembeli(total int) string {
if total < 0 {
return "tidak valid"
}
if total >= 10_000_000 {
return "platinum"
}
if total >= 1_000_000 {
return "gold"
}
return "reguler"
}
// KuponValid memeriksa kecocokan kode kupon dengan daftar kupon aktif.
func KuponValid(kode string) bool {
if kode == "" {
return false
}
for _, k := range []string{"HEMAT10", "GOSPORT", "NEWYEAR"} {
if kode == k {
return true
}
}
return false
}
func main() {
fmt.Println(HitungDiskon(150000, "gold"))
fmt.Println(KategoriPembeli(2_500_000))
fmt.Println(KuponValid("HEMAT10"))
}