Tes Aplikasi Go dengan GoMock dan GoConvey

Di SIRCLO, bahasa pemrograman Go digunakan untuk mengembangkan backend untuk salah satu produk baru kami. Untuk keperluan testing, banyak tool dan library yang dikembangkan oleh tim Go sendiri maupun komunitas. Pada tulisan ini, kami ingin berbagi mengenai bagaimana kami melakukan testing pada produk berbahasa Go di SIRCLO.

Testing framework: GoConvey

Go sudah menyediakan framework bawaan pada package testing. Setelah bereksplorasi, kami memilih menggunakan GoConvey karena memiliki beberapa fitur memudahkan, misalnya nested test.

Sebagai contoh, misalkan kita ingin melakukan integration test dengan 2 buah skenario berikut ini untuk melakukan operasi insert sebuah objek akun ke dalam database:

  • username akun tersebut valid, dan
  • username akun tersebut tidak valid.

Karena integration test ini menggunakan koneksi database sungguhan (bukan mock), kita ingin melakukan truncate pada tabel akun sebelum setiap skenario, agar setiap skenario menjadi independen.

Menggunakan fitur built-in pada Go, kedua skenario tersebut dapat ditulis kira-kira seperti berikut ini:

func setUpDatabase() {
// kode truncate tabel akun di sini
}
func TestAccount_UsernameValid(t *testing.T) {
setUpDatabase()

// kode test untuk skenario username valid di sini
}
func TestAccount_UsernameInValid(t *testing.T) {
setUpDatabase()

// kode test untuk skenario username invalid di sini
}

Pattern di atas memiliki beberapa kelemahan:

  • Kita tidak boleh lupa untuk memanggil setUpDatabase() di awal setiap tes.
  • Penamaan fungsi-fungsi untuk setiap skenario tes terlihat kurang fleksibel.

Dengan menggunakan GoConvey, kedua kelemahan tersebut dapat teratasi. Bandingkan dengan kedua skenario yang telah ditulis ulang menggunakan GoConvey sebagai berikut:

func setUpDatabase() {
// kode truncate tabel akun di sini
}
func TestAccount(t *testing.T) {
Convey("Account, t, func() {
setUpDatabase()

Convey("When the username is valid", func() {
// kode test untuk skenario username valid di sini
})
        Convey("When the username is invalid", func() {
// kode test untuk skenario username valid di sini
})
})
}

Enaknya, GoConvey kompatibel dengan framework testing bawaan Go, sehingga kita masih bisa menjalankan tes-tes dengan perintah go test .

Testing assertions framework: masih GoConvey

Testing assertion secara idiomatik di Go sebenarnya berbentuk seperti berikut ini:

if expected != actual {
t.Errorf("expected %s, got %s", expected, actual)
}

Kami merasa pattern di atas error-prone dan merepotkan untuk ditulis (misalnya: baik expected muapun actual ditulis dua kali). Oleh karena itu, pada awalnya kami mencoba untuk membungkus kode di atas dalam suatu fungsi, kira-kira seperti ini:

func check(t *testing.T, expected, actual interface{}) {
if expected != actual {
t.Errorf("expected %s, got %s", expected, actual)
}
}

sehingga kami bisa melakukan assertion dengan pemanggilan seperti ini:

check(t, "sirclo", account1.Username)

Akan tetapi, kami masih merasa assertion di atas kurang ekspresif. Oleh karena itu, kami memanfaatkan assertion-assertion yang sudah disediakan oleh GoConvey. Tidak hanya assertion mengenai equality saja, namun juga mengenai banyaknya elemen array, pengecekan substring pada string, dan lain-lain. Misalnya:

So(account1, ShouldNotBeNil)
So(account1.Username, ShouldEqual, "sirclo")
So(account1.Emails, ShouldNotBeEmpty)

dan lain sebagainya. Kelebihannya adalah, ketika assertion tidak terpenuhi, pesan kesalahannya lebih memudahkan untuk debugging. Misalnya, pada contoh di atas, jika arrayaccount1.Emails tidak kosong, akan ditampilkan banyaknya elemen sesungguhnya.

Mocking/stubbing framework: GoMock

Kami menggunakan dependency injection (DI) pada hampir semua komponen kode, salah satunya untuk memudahkan testing. Dengan DI, kita bisa meng-inject mock dependency pada komponen yang sedang dites.

Sebagai contoh, misalkan kita memiliki komponen bernama Marketplace yang bergantung pada komponen Database. Kita ingin mengetes kalau fungsi Marketplace.CreateOrder() akan melakukan operasi INSERT pada Database dengan parameter yang tepat.

Dengan GoMock, pertama-tama kita akan men-generate sebuah berkas mock untuk tipe Database tersebut (misalkan tipe tersebut terdapat pada db/database.go):

mockgen -source=db/database.go > tests/mock_db/mock_database.go

Perintah di atas akan membuat sebuah berkas mock_database.go yang akan digunakan sebagai dependency dari Marketplace pada tes.

Akhirnya, skenario tes di atas dapat diekspresikan dengan melakukan verifikasi pada mock seperti di bawah ini:

mockDb := mock_db.NewMockDatabase(...)
mp := marketplace.NewMarketplace(mockDb)
Convey("CreateOrder()", func() {
Convey("It inserts the correct order to database", func() {
mockDb.EXPECT().Insert(Order{BuyerUsername: "budi"})
mp.CreateOrder("budi")
})
})

Sebagai contoh, apabila pemanggilan mp.CreateOrder("budi") pada kenyataannya tidak memanggil mockDb.Insert() dengan argumen yang diharapkan, maka GoMock akan melaporkan bahwa pemanggilan tersebut tidak terjadi.

Kesimpulan

Dengan memanfaatkan library dan framework yang tepat, melakukan testing pada bahasa Go bisa dilakukan dengan produktif dan efisien. Di SIRCLO, kami merasa bahwa pilihan-pilihan library seperti yang dijelaskan pada artikel ini menunjang produktivitas testing bagi engineer SIRCLO dengan baik.