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:

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:

bash
go run diskon.go

Output yang Dihasilkan:

text
30000
gold
true

Mari kita telaah bagian-bagian kuncinya:

  1. Baris 7-16 (func HitungDiskon): satu switch dengan tiga cabang — masing-masing cabang adalah statement terpisah yang hanya terhitung tercakup bila test mengirim tingkat member yang sesuai.
  2. Baris 19-30 (func KategoriPembeli): empat cabang return; cabang tidak valid dan platinum sengaja belum punya test agar muncul sebagai celah coverage.
  3. Baris 33-43 (func KuponValid): dua cabang awal kosong/tidak valid dan satu loop pencarian kupon.
  4. Baris 45-49 (func main): tiga pencetakan sederhana — nanti terlihat bahwa go test tidak pernah menjalankan main, sehingga fungsi ini tampil 0%.

Test-nya berada pada diskon_test.go:

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:

bash
go test -v ./...

Output yang Dihasilkan:

text
=== 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.002s

Mengukur dengan go test -cover #

Tambahkan flag -cover untuk melihat angka cakupan:

bash
go test -cover ./...

Output yang Dihasilkan:

text
ok  	tutorial-website/content/go/testing/coverage	0.002s	coverage: 75.0% of statements

Angka 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:

bash
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out

Perintah 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:

text
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:

text
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 1

Setiap 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

diskon_test.go

Unduh berkas
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)
		}
	}
}

diskon.go

Unduh berkas
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"))
}