DEV Community

Cover image for Unit Test: Mock Obat Pusing atau Yang Buat Pusing?
Puagee
Puagee

Posted on

Unit Test: Mock Obat Pusing atau Yang Buat Pusing?

Kalo kita udah mulai ngerjain sebuah sistem, biasanya kita juga akan diminta untuk buat Unit Test-nya juga. Tapi agar bisa sebuah Unit Test bisa tercover dengan baik dan mudah, kita perlu belajar nih tentang Mock. Pastikan sebelum kalian belajar tentang dan cara menggunakan Mock, kalian sudah belajar menggunakan Interface dan konsep Encapsulation. Lebih baik lagi kalau kalian sudah belajar tentang Clean Architecture.

What is Mock & How it works? πŸ™„

Kalian tau stuntman? Mock bisa kita analogikan seperti stuntman atau stunt double dalam dunia unit test. Seperti halnya stuntman yang menggantikan aktor asli dalam adegan berbahaya atau sulit, mock juga digunakan untuk menggantikan objek asli atau komponen eksternal yang bergantung pada code yang ingin diuji. Sehingga dengan adanya mock sebagai pengganti objek asli dalam pengujian, kita bisa tuh mengontrol perilaku dan respons yang diberikan oleh mock selama pengujian. Mirip banget dengan stuntman yang mengikuti skenario yang ditentuin oleh sutradara, mock juga bisa diatur untuk mengikuti skenario pengujian yang kita inginkan, sehingga kita bisa menguji code kita secara efisien dan efektif.

Nah, jadi kalo misalkan kita mau membuat unit test pada sebuah code, yang mana didalam code itu ada kebutuhan untuk mengakses database. Kan pasti susah yak, kalau setiap kali kita jalanin unit test yang kita buat, kita harus siapkan database-nya juga. Karena itu kita tinggal buat aja mock untuk database ini, biar code yang kita buat enggak bener-bener berkomunikasi dengan database yang asli.

Before use Mock, take a notes about this πŸ“

Penggunaan mock enggak cuman buat seluruh code yang ada dependency ke luar code yang kalian buat. Oleh karena itu, pastiin kalian sudah belajar Interface dan Encapsulation karena mock bisa digunakan untuk code apapun yang sudah kita pisah-pisahkan menjadi komponen lebih kecil yang mana komunikasi antar code ini sudah menggunakan sebuah kontrak yang disebut Interface. Kalau kalian sudah belajar mengenai Clean Architecture, maka code kalian akan terpisah-pisah dengan baik tuh, salah satu layernya adalah code yang hanya berisi logika bisnis biasanya disebut Usecase Layer. Usecase ini nantinya akan punya mock sendiri yang akan dipakai di bagian Handler Layer, jadinya di Handler enggak perlu tuh bener-bener panggil Usecase yang isinya ada segudang logika panjang. Oleh karena itu kalian perlu paham akan struktur code kalian sendiri, yah meskipun gak jarang sih kita bingung pas lihat code kita sendiri. β€œLoh kok gini? Ini code maksudnya gimana yahπŸ˜…β€.

Setelah paham akan struktur code, kalian perlu untuk tahu apa yang ingin kalian uji. Kira-kira skenario apa yang ingin kalian buat dalam pengujian code kalian. Misalnya kalian punya code yang gunanya buat ambil data produk dari ecommerce. Nah salah satu skenario yang bisa kalian buat adalah jika website ecommerce yang akan kalian ambil data produknya tiba-tiba down, maka seharusnya code yang kita buat tidak crash dan hanya akan return error.

Kalo kalian jeli banget, skenario kalian biasanya akan banyak dan beragam banget. Dan itulah yang akan membuat kalian semakin pede dengan code kalian. Coba deh kalian baca dan bongkar dokumentasi seperti PRD atau Flowchart, biar kalian tau tuh β€œOh nanti input-nya bakal kayak gini ya, terus dengan ekspektasi dari flow ini akan menghasilkan output A”. Pas kalian baca dokumentasi otomatis nih kalian punya kesempatan buat nemuin bug dari flow yang ada. Weee, tambah cakep lagi sistem yang kalian buat, kualitas meningkat dan semakin kredibel.

Using Plain Mock (No library) πŸ’ͺ

Untuk pake mock di Golang, kita perlu menggunakan interface sebagai kontrak kerja akan sebuah proses. Di sini kita akan coba membuat mock untuk sebuah proses penyimpanan data ke database, dengan interface sebagai berikut

type Database interface {
        Save(data string) error
}
Enter fullscreen mode Exit fullscreen mode

Dengan interface diatas, mock yang bisa dibuat kurang lebih kayak gini

type MockDatabase struct{
        SaveFunc func(data []byte) error
}

func (m *MockDatabase) Save(data string) error {
        return m.SaveFunc(data)
}
Enter fullscreen mode Exit fullscreen mode

SaveFunc berfungsi agar kalian bisa memiliki kontrol akan perilaku dari mock. Kalo kalian gak ada SaveFunc di dalam struct MockDatabase sebenernya bisa juga mocknya berjalan, tapi perilakunya akan lebih statis. Misalnya contoh dibawah ini

type MockDatabase struct{}

func (m *MockDatabase) Save(data string) error {
        if data == "test1" {
                return fmt.Errorf("error test1")
        }

        return nil
}
Enter fullscreen mode Exit fullscreen mode

atau mungkin bakal lebih parah lagi kayak gini

type MockDatabase1 struct{}

func (m *MockDatabase1) Save(data string) error {
        return fmt.Errorf("error test1")
}

type MockDatabase2 struct{}

func (m *MockDatabase2) Save(data string) error {
        return nil
}
Enter fullscreen mode Exit fullscreen mode

Jadi ribet banget kan? Kurang efektif dan efisien juga. Nah makanya penggunaan SaveFunc disini krusial biar memudahkan kita dalam pembuatan unit test.

Untuk cara pakenya, kalian tinggal define MockDatabase dan isi deh SaveFunc sesuai kebutuhan. Nih contohnya

func TestSaveData(t *testing.T) {
    mockDB := &MockDatabase{
        SaveFunc: func(data []byte) error {
            return nil
        },
    }

    // Jalanin code yang menggunakan mockDB.Save()
    // Misalnya
    // dep := saveDataImpl{
    //  db: mockDB,
    // }
    // dep.SaveData("test1")
}
Enter fullscreen mode Exit fullscreen mode

Gimana cakep kan? 😎

Huh? Why we need to use library then?

Meskipun kalian bisa buat mock sendiri, tapi kalo kalian pake library atau package yang khusus untuk mock kalian bisa dapet sederet benefit:

  1. Mengurangi boilerplate, iyesss boilerplate pasti bakal berkurang dan yang perlu kalian tahu kalian bisa melakukan proses pembuatan mock secara otomatis. Interface berubah? gak perlu takut lagi deh, tinggal jalanin beberapa command aja, nanti mocknya auto update deh. Aseekk!!
  2. Maintain jadi lebih gampang, yak betul jadi lebih gampang buat kalian maintain, kenapa? Karena dengan menggunakan dedicated package kalian pastinya akan mempunyai mock yang standar entah dari segi penamaan ataupun style code.
  3. Meningkatkan *coverage test, kok bisa naik coverage-nya? sebenernya nggak otomatis naik, cuman akan membantu kalian untuk lebih mudah memenuhi beberapa skenario yang susah untuk dibuat dengan menggunakan mock yang manual. Coba deh, kalau simulasinya lebih gampang pasti kalian bisa *handle tuh edge cases yang ada di pikiran kalian.
  4. Integrasi yang lebih baik dengan testing framework lainnya, kebanyakan package mock yang populer bisa diintegrasikan dengan mudah ke testing framework lainnya.

Package untuk mock itu ada buanyakk banget, coba kalian cari deh di https://awesome-go.com/. Tapi biar gak bingung kita ambil satu aja deh, dan yang akan kita pakai itu package mock dari golang langsung yak namanya GoMock (https://github.com/golang/mock).

Gomock - The SaviorπŸ˜‡

Yok mari kita langsung cus kita coba pakai gomock, tapi masih menggunakan Interface yang sama.

// Aku copy-in nih biar gak perlu scroll ke atas-atas :D
type Database interface {
        Save(data string) error
}
Enter fullscreen mode Exit fullscreen mode

Sebelum itu kita perlu install yang namanya mockgen yang gunanya buat generate mock secara otomatis based on interface yang dibuat.

Kalo versi go kalian < 1.16

GO111MODULE=on go get github.com/golang/mock/mockgen@v1.6.0
Enter fullscreen mode Exit fullscreen mode

Kalo versi go kalian 1.16+

go install github.com/golang/mock/mockgen@v1.6.0
Enter fullscreen mode Exit fullscreen mode

Cara pakai mockgen cukup simple

mockgen -package=[PACKAGE_NAME] -source=[INTERFACE_FILE_LOCATION] -destination=[MOCK_FILE_DESTINATION]
Enter fullscreen mode Exit fullscreen mode

Untuk command β€œpackage” sendiri itu opsional karena itu tergantung struktur folder yang kalian pakai. Kalo kalian itu menggabungkan seluruh mock menjadi 1 folder sendiri, ya nantinya package name-nya jadi sama semua nantinya. Contohnya kayak dibawah

mockgen -package=mock -source=infra/db.go -destination=mock/infra/db.go
Enter fullscreen mode Exit fullscreen mode

TIPS πŸ’‘
Better untuk kumpulin semua command mockgen ke satu file biar nanti kita bisa autogenerate seluruh interface yang ada di project kita. Kalo dari aku sendiri biasanya taruh ke SH File terus file ini nantinya akan dipanggil oleh Makefile.
SH File β†’ https://docs.fileformat.com/id/programming/sh/
Makefile β†’ https://makefiletutorial.com/

Setelah file mock sudah ter-generate, jangan lupa juga untuk install package gomock di project kalian

go get github.com/golang/mock/gomock
Enter fullscreen mode Exit fullscreen mode
func TestSaveData(t *testing.T) {
    ctrl := gomock.NewController(t)
        defer ctrl.Finish()

        m := mock.NewMockDatabase(ctrl)
        m.EXPECT().Save("test1").Return(nil)

        // Jalanin code yang menggunakan mock database
        // Misalnya 
        // dep := saveDataImpl{
        //  db: m,
        // }
        // dep.SaveData("lala")
}
Enter fullscreen mode Exit fullscreen mode

Pastikan untuk selalu inisialisasi ekspektasi mock dengan rapi, kalo rekomendasi dari aku silahkan kalian gunakan Table Driven ya! Yang tau Table Drive bisa komen di bawah yak! Clue-nya kalo kalian pakai Visual Studio Code, Table Drive ini bisa di-generate loh tinggal klik kanan Generate Unit Test jadi deh skeleton unit test-nya.

Gomock not good enough?πŸ˜•

Selain gomock, ada beberapa library lain yang bisa kalian explore

  1. Testify, library ini sebenernya enggak cuman buat mock tapi juga bisa membuat unit test kita lebih baik dan readable yaitu dengan assert package. Tapi ada kabar baik buat kalian, karena kalo kalian mau tetep pake testify tapi untuk mock-nya auto generated kalian tinggal pake 1 package tambahan yaitu https://github.com/vektra/mockery
  2. Ginkgo, library ini cara pakai-nya agak berbeda dengan testify dan gomock. Tapi bagi kalian yang pernah menggunakan bahasa pemrograman lain mungkin akan cukup familiar dengan cara penulisannya karena mirip dengan beberapa library unit test lain seperti Jasmine dan Quick. Karena Ginkgo ini terbilang expressive dan kadang-kadang juga dikategorikan sebagai Behavior-Driven Development (BDD)

Fun Fact βœ…

Kita ini manusia gampang banget untuk ngebuat error, jadi sebenernya banyak sekali bug atau dosa dari code itu gara-gara manusia. Ada sebuah penelitian yang menyatakan kalau β€œhuman factor” error itu nyata banget dan gak bisa dihindari. Nah kalian penasaran kan, seberapa sering sih kita melakukan error? Kalian bisa baca detailnya di https://lifetime-reliability.com/tutorials/human_error_rate_table_insights/. Itung-itung kalian belajar bahasa enggress yak, karena kalau mau hidup di dunia teknologi ya harus bisa bahasa inggris. Minimal kalau kita ada bug di code kita, kita bisa tanya tuh di stackoverflow.

Masih bingung? yuk kita diskusi di kolom komentar.

Special Thanks πŸ‘

Cover Image by Kristijan Arsov on Unsplash

Top comments (0)