Published on

Tips dan trik unit test di Go

Authors

Unit Test memang tidak bisa dilepaskan dari proses pengembangan software. Namun seringkali dalam pembuatan Unit Test di Go terjadi banyak repetisi yang tidak perlu dan Unit Test yang tidak dikelola dengan baik. Salah satu contoh kasus yang paling banyak ditemui dalam pembuatan Unit Test yaitu tidak dipisahkannya logic dan data sehingga ketika penambahan data test terdapat penambahan pula logic.

Contoh Kode

Sebelum memulai lebih lanjut berikut adalah contoh kode untuk membantu memahami tulisan ini.

  • Struktur direktori

_3
$ tree .
_3
├── add.go
_3
└── add_test.go

  • File add.go

_6
package main
_6
_6
// Tambah merupakan fungsi sederhana penjumlahan
_6
func Tambah(a, b int)int{
_6
return a+b
_6
}

  • File add_test.go

_15
package main
_15
_15
import "testing"
_15
_15
func TestBasic(t *testing.T){
_15
if Tambah(1,1) != 2{
_15
t.Error("seharusnya 2")
_15
}
_15
_15
if Tambah(1,2) != 3{
_15
t.Error("seharusnya 3")
_15
}
_15
_15
// kode berulang sebanyak jumlah data test
_15
}

Hasil running Unit Test


_5
$ go test -v
_5
=== RUN TestBasic
_5
--- PASS: TestBasic (0.00s)
_5
PASS
_5
ok github.com/h4ckm03d/blog-codes/golang101/6-tips-trik-unit-test 0.002s

Memanfaatkan array of struct

Contoh Unit Test diatas merupakan contoh paling sederhana, jika yang sederhana saja sudah terlalu verbose dan susah dikelola bagaimana jika kode dalam suatu package semakin besar? Salah satu cara untuk memisahkan logic unit test dengan data yaitu menggunakan unit test array of struct untuk data dan tinggal menambahkan looping data ketika pengecekan. Tambahkan fungsi dibawah ini ke dalam add_test.go


_20
func TestB(t *testing.T) {
_20
testData := []struct {
_20
name string
_20
inputA int
_20
inputB int
_20
result int
_20
}{
_20
{"1+1", 1, 1, 2},
_20
{"1+2", 1, 2, 3},
_20
{"1+3", 1, 3, 4},
_20
}
_20
_20
for _, tc := range testData {
_20
t.Run(tc.name, func(t *testing.T) {
_20
if Tambah(tc.inputA, tc.inputB) != tc.result {
_20
t.Errorf("Seharusnya %d", tc.result)
_20
}
_20
})
_20
}
_20
}

Jika Unit Test dijalankan maka hasilnya seperti berikut:


_13
$ go test -v
_13
=== RUN TestBasic
_13
--- PASS: TestBasic (0.00s)
_13
=== RUN TestB
_13
=== RUN TestB/1+1
_13
=== RUN TestB/1+2
_13
=== RUN TestB/1+3
_13
--- PASS: TestB (0.00s)
_13
--- PASS: TestB/1+1 (0.00s)
_13
--- PASS: TestB/1+2 (0.00s)
_13
--- PASS: TestB/1+3 (0.00s)
_13
PASS
_13
ok github.com/h4ckm03d/blog-codes/golang101/6-tips-trik-unit-test 0.002s

Dalam TestB diatas ketika data test bertambah, kita hanya perlu menambahkan kedalam array testData tanpa perlu merubah logic sehingga kode lebih mudah dibaca dan dimodifikasi. struct dalam peubah testData terdiri dari 3 bagian label test(name), input data (inputA, inputB), dan output dari fungsi (result). Untuk input data dan output bisa diganti sesuai dengan fungsi yang ditest. Hasil running dari unit test juga lebih jelas jika menggunakan metode ini karena penggunaan label membantu tracking ketika ada kesalahan. Misalkan testData 1 ditambah 1 diubah menjadi 7 untuk mengetahui contoh jika unit test menghasilkan error, hasilnya sebagai berikut:


_15
$ go test -v
_15
=== RUN TestBasic
_15
--- PASS: TestBasic (0.00s)
_15
=== RUN TestB
_15
=== RUN TestB/1+1
_15
=== RUN TestB/1+2
_15
=== RUN TestB/1+3
_15
--- FAIL: TestB (0.00s)
_15
--- FAIL: TestB/1+1 (0.00s)
_15
add_test.go:34: Seharusnya 7
_15
--- PASS: TestB/1+2 (0.00s)
_15
--- PASS: TestB/1+3 (0.00s)
_15
FAIL
_15
exit status 1
_15
FAIL github.com/h4ckm03d/blog-codes/golang101/6-tips-trik-unit-test 0.005s

Karena output diubah menjadi 7 sehingga unit testnya fail, akan tetapi karena menggunakan label kita segera mengetahui data apa yang salah sehingga bug fix lebih mudah.

Fatal VS Error

Pada fungsi TestBasic diatas menggunakan t.Error("seharusnya 2"), kenapa menggunakan t.Error bukan menggunakan t.Fatal ? Penggunaan error diatas karena ketika terjadi kesalahan misalkan output salah atau tidak sesuai maka unit test tidak akan berhenti tetapi melanjutkan sampai semua proses dalam fungsi tersebut selesai. Jika menggunakan fatal maka ketika t.Fatal atau t.Fatalf dipanggil maka proses dihentikan. Dalam hal ini ketika 1 + 1 terjadi error maka proses berhenti dan tidak dilanjutkan pengecekan 1 + 2. Berikut contoh perbedaan penggunaan fatal dan error.


_23
func TestBasicFatal(t *testing.T) {
_23
if Tambah(1, 1) != 3 {
_23
t.Error("seharusnya 2")
_23
}
_23
_23
if Tambah(1, 2) != 4 {
_23
t.Error("seharusnya 3")
_23
}
_23
_23
// kode berulang sebanyak jumlah data test
_23
}
_23
_23
func TestBasicError(t *testing.T) {
_23
if Tambah(1, 1) != 3 {
_23
t.Error("seharusnya 2")
_23
}
_23
_23
if Tambah(1, 2) != 4 {
_23
t.Error("seharusnya 3")
_23
}
_23
_23
// kode berulang sebanyak jumlah data test
_23
}

Hasil go test -v sebagai berikut


_21
$ go test -v
_21
=== RUN TestBasic
_21
--- PASS: TestBasic (0.00s)
_21
=== RUN TestBasicFatal
_21
--- FAIL: TestBasicFatal (0.00s)
_21
add_test.go:21: seharusnya 2
_21
=== RUN TestBasicError
_21
--- FAIL: TestBasicError (0.00s)
_21
add_test.go:33: seharusnya 2
_21
add_test.go:37: seharusnya 3
_21
=== RUN TestB
_21
=== RUN TestB/1+1
_21
=== RUN TestB/1+2
_21
=== RUN TestB/1+3
_21
--- PASS: TestB (0.00s)
_21
--- PASS: TestB/1+1 (0.00s)
_21
--- PASS: TestB/1+2 (0.00s)
_21
--- PASS: TestB/1+3 (0.00s)
_21
FAIL
_21
exit status 1
_21
FAIL github.com/h4ckm03d/blog-codes/golang101/6-tips-trik-unit-test 0.004s

Ketika TestBasicFatal dijalankan maka ketika ada error proses berhenti berbeda dengan TestBasicError, meskipun ada error proses dilanjutkan sampai selesai.

Dengan pemisahan kode Unit Test dan penggunaan error/fatal yang tepat kita bisa lebih optimal memanfaatkan unit test. Semoga bermanfaat dan sampai jumpa lagi di tulisan selanjutnya.