1. Pengenalan Unit Testing

Unit testing merujuk pada pemeriksaan dan validasi unit terkecil yang bisa diuji dalam sebuah program, seperti fungsi atau metode dalam bahasa Go. Unit testing memastikan bahwa kode berfungsi sesuai harapan dan memungkinkan pengembang untuk melakukan perubahan pada kode tanpa sengaja merusak fungsionalitas yang sudah ada.

Dalam proyek Golang, pentingnya unit testing tak perlu diragukan lagi. Pertama, dapat meningkatkan kualitas kode dan memberikan kepercayaan diri lebih kepada pengembang dalam melakukan perubahan pada kode. Kedua, unit testing dapat berfungsi sebagai dokumentasi untuk kode, menjelaskan perilaku yang diharapkan. Selain itu, menjalankan unit test secara otomatis dalam lingkungan integrasi berkelanjutan dapat dengan cepat menemukan bug baru yang muncul, sehingga meningkatkan stabilitas perangkat lunak.

2. Melakukan Pengujian Dasar Menggunakan Paket testing

Pustaka standar bahasa Go termasuk paket testing, yang menyediakan alat dan fungsionalitas untuk menulis dan mengeksekusi pengujian.

2.1 Membuat Kasus Uji Pertama Anda

Untuk menulis fungsi uji, Anda perlu membuat file dengan akhiran _test.go. Misalnya, jika file kode sumber Anda bernama calculator.go, file uji Anda harus dinamai calculator_test.go.

Selanjutnya, saatnya untuk membuat fungsi uji. Sebuah fungsi uji perlu mengimpor paket testing dan mengikuti format tertentu. Berikut contoh sederhana:

// calculator_test.go
package calculator

import (
	"testing"
	"fmt"
)

// Uji fungsi penambahan
func TestAdd(t *testing.T) {
	result := Add(1, 2)
	expected := 3

if result != expected {
		t.Errorf("Seharusnya %v, tapi dapat %v", expected, result)
	}
}

Dalam contoh ini, TestAdd adalah fungsi uji yang menguji sebuah fungsi Add yang fiktif. Jika hasil dari fungsi Add cocok dengan hasil yang diharapkan, uji akan berhasil; jika tidak, t.Errorf akan dipanggil untuk mencatat informasi tentang kegagalan uji.

2.2 Memahami Aturan Penamaan dan Tanda Tangan Fungsi Uji

Fungsi uji harus diawali dengan Test, diikuti oleh string non-huruf kecil apa pun, dan satu-satunya parameternya harus berupa pointer ke testing.T. Seperti yang ditunjukkan dalam contoh, TestAdd mengikuti aturan penamaan dan tanda tangan yang benar.

2.3 Menjalankan Kasus Uji

Anda dapat menjalankan kasus uji menggunakan perintah baris perintah. Untuk sebuah kasus uji tertentu, jalankan perintah berikut:

go test -v // Menjalankan uji di direktori saat ini dan menampilkan output detail

Jika Anda ingin menjalankan kasus uji tertentu, Anda dapat menggunakan flag -run diikuti dengan ekspresi reguler:

go test -v -run TestAdd // Menjalankan hanya fungsi uji TestAdd

Perintah go test akan secara otomatis menemukan semua file _test.go dan mengeksekusi setiap fungsi uji yang memenuhi kriteria. Jika semua uji berhasil, Anda akan melihat pesan mirip PASS di baris perintah; jika ada uji yang gagal, Anda akan melihat FAIL beserta pesan kesalahan yang sesuai.

3. Menulis Kasus Uji

3.1 Melaporkan Kesalahan dengan Menggunakan t.Errorf dan t.Fatalf

Dalam bahasa Go, kerangka pengujian menyediakan berbagai metode untuk melaporkan kesalahan. Dua fungsi yang paling umum digunakan adalah Errorf dan Fatalf, keduanya adalah metode dari objek testing.T. Errorf digunakan untuk melaporkan kesalahan dalam uji tapi tidak menghentikan kasus uji saat ini, sementara Fatalf menghentikan uji saat ini segera setelah melaporkan kesalahan. Penting untuk memilih metode yang sesuai berdasarkan kebutuhan pengujian.

Contoh penggunaan Errorf:

func TestAdd(t *testing.T) {
    got := Add(1, 2)
    want := 3
    if got != want {
        t.Errorf("Add(1, 2) = %d; seharusnya %d", got, want)
    }
}

Jika Anda ingin menghentikan uji segera setelah mendeteksi kesalahan, Anda dapat menggunakan Fatalf:

func TestSubtract(t *testing.T) {
    got := Subtract(5, 3)
    if got != 2 {
        t.Fatalf("Subtract(5, 3) = %d; seharusnya 2", got)
    }
}

Secara umum, jika kesalahan akan menyebabkan kode selanjutnya tidak dieksekusi dengan benar atau kegagalan uji dapat dikonfirmasi sebelumnya, disarankan untuk menggunakan Fatalf. Jika tidak, disarankan untuk menggunakan Errorf untuk mendapatkan hasil uji yang lebih komprehensif.

3.2 Mengatur Subtes dan Menjalankan Subtes

Di Go, kita dapat menggunakan t.Run untuk mengatur subtes, yang membantu kita menulis kode uji dalam cara yang lebih terstruktur. Subtes dapat memiliki Setup dan Teardown mereka sendiri dan dapat dijalankan secara individual, memberikan fleksibilitas yang besar. Ini sangat berguna untuk melakukan uji kompleks atau uji parameter.

Contoh penggunaan subtes t.Run:

func TestKalikan(t *testing.T) {
    testcases := []struct {
        name           string
        a, b, expected int
    }{
        {"2x3", 2, 3, 6},
        {"-1x-1", -1, -1, 1},
        {"0x4", 0, 4, 0},
    }

    for _, tc := range testcases {
        t.Run(tc.name, func(t *testing.T) {
            if got := Kalikan(tc.a, tc.b); got != tc.expected {
                t.Errorf("Kalikan(%d, %d) = %d; harapannya %d", tc.a, tc.b, got, tc.expected)
            }
        })
    }
}

Jika kita ingin menjalankan subtes bernama "2x3" secara individual, kita dapat menjalankan perintah berikut dalam baris perintah:

go test -run TestKalikan/2x3

Harap dicatat bahwa nama subtes bersifat case-sensitive.

4. Persiapan Sebelum dan Setelah Pengujian

4.1 Persiapan dan Kerja Bersih

Ketika melakukan pengujian, kita sering perlu menyiapkan beberapa keadaan awal untuk uji (seperti koneksi database, pembuatan file, dll.), dan begitu juga, kita perlu melakukan beberapa kerja bersih setelah pengujian selesai. Di Go, biasanya kita melakukan Setup dan Teardown langsung di dalam fungsi uji, dan fungsi t.Cleanup memberikan kita kemampuan untuk mendaftarkan fungsi panggilan kerja bersih.

Berikut adalah contoh sederhana:

func TestDatabase(t *testing.T) {
    db, err := SetupDatabase()
    if err != nil {
        t.Fatalf("persiapan gagal: %v", err)
    }

    // Daftarkan panggilan kerja bersih untuk memastikan koneksi database ditutup ketika pengujian selesai
    t.Cleanup(func() {
        if err := db.Close(); err != nil {
            t.Errorf("gagal menutup database: %v", err)
        }
    })

    // Lakukan pengujian...
}

Di dalam fungsi TestDatabase, kita pertama-tama memanggil fungsi SetupDatabase untuk menyiapkan lingkungan uji. Kemudian, kita menggunakan t.Cleanup() untuk mendaftarkan fungsi yang akan dipanggil setelah pengujian selesai untuk melakukan kerja bersih, dalam contoh ini, menutup koneksi database. Dengan cara ini, kita dapat memastikan bahwa sumber daya tersebut dilepaskan dengan benar terlepas dari keberhasilan atau kegagalan pengujian.

5. Meningkatkan Efisiensi Pengujian

Meningkatkan efisiensi pengujian dapat membantu kita mengembangkan dengan cepat, dengan cepat menemukan isu, dan memastikan kualitas kode. Di bawah ini, kita akan membahas cakupan pengujian, uji berbasis tabel, dan penggunaan mock untuk meningkatkan efisiensi pengujian.

5.1 Cakupan Pengujian dan Alat Terkait

Alat go test menyediakan fitur cakupan pengujian yang sangat berguna, yang membantu kita memahami bagian mana dari kode yang dicakup oleh kasus uji, sehingga menemukan area kode yang tidak dicakup oleh kasus uji.

Dengan menggunakan perintah go test -cover, Anda dapat melihat persentase cakupan pengujian saat ini:

go test -cover

Jika Anda ingin memahami lebih detail langsung kode mana yang dieksekusi dan mana yang tidak, Anda dapat menggunakan parameter -coverprofile, yang menghasilkan file data cakupan. Kemudian, Anda dapat menggunakan perintah go tool cover untuk menghasilkan laporan cakupan uji yang mendetail.

go test -coverprofile=coverage.out
go tool cover -html=coverage.out

Perintah di atas akan membuka laporan web, yang memperlihatkan secara visual kode mana yang dicakup oleh uji dan mana yang tidak. Hijau menunjukkan kode yang dicakup oleh uji, sementara merah menunjukkan kode yang tidak dicakup.

5.2 Menggunakan Mock

Dalam pengujian, sering kali kita menghadapi skenario di mana kita perlu mensimulasikan ketergantungan eksternal. Mock dapat membantu kita mensimulasikan ketergantungan ini, menghilangkan kebutuhan untuk bergantung pada layanan atau sumber eksternal spesifik dalam lingkungan pengujian.

Ada banyak alat mock dalam komunitas Go, seperti testify/mock dan gomock. Alat-alat ini biasanya menyediakan serangkaian API untuk membuat dan menggunakan objek mock.

Berikut adalah contoh dasar penggunaan testify/mock. Hal pertama yang harus dilakukan adalah mendefinisikan sebuah antarmuka dan versi mockingnya:

type DataService interface {
    FetchData() (int, error)
}

type MockDataService struct {
    mock.Mock
}

func (m *MockDataService) FetchData() (int, error) {
    args := m.Called()
    return args.Int(0), args.Error(1)
}

Dalam pengujian, kita dapat menggunakan MockDataService untuk menggantikan layanan data aktual:

func TestSomething(t *testing.T) {
    mockDataSvc := new(MockDataService)
    mockDataSvc.On("FetchData").Return(42, nil) // Mengkonfigurasi perilaku yang diharapkan

    result, err := mockDataSvc.FetchData() // Menggunakan objek mock
    assert.NoError(t, err)
    assert.Equal(t, 42, result)

    mockDataSvc.AssertExpectations(t) // Memverifikasi apakah perilaku yang diharapkan terjadi
}

Melalui pendekatan di atas, kita dapat menghindari ketergantungan pada layanan eksternal, panggilan database, dll. dalam pengujian. Hal ini dapat mempercepat eksekusi pengujian dan membuat pengujian kita lebih stabil dan dapat diandalkan.

6. Teknik Pengujian Lanjutan

Setelah menguasai dasar-dasar pengujian unit Go, kita dapat menjelajahi beberapa teknik pengujian lebih lanjut, yang membantu dalam membangun perangkat lunak yang lebih kokoh dan meningkatkan efisiensi pengujian.

6.1 Pengujian Fungsi Private

Dalam Golang, fungsi private biasanya merujuk pada fungsi yang tidak diekspor, yaitu fungsi yang nama-namanya dimulai dengan huruf kecil. Biasanya, kita lebih suka menguji antarmuka publik, karena mereka mencerminkan kegunaan kode. Namun, ada kasus di mana langsung menguji fungsi private juga masuk akal, seperti ketika fungsi private memiliki logika kompleks dan dipanggil oleh beberapa fungsi publik.

Menguji fungsi private berbeda dari menguji fungsi publik karena mereka tidak dapat diakses dari luar paket. Teknik umum adalah menulis kode pengujian dalam paket yang sama, memungkinkan akses ke fungsi private.

Berikut adalah contoh sederhana:

// calculator.go
package calculator

func add(a, b int) int {
    return a + b
}

Berkas pengujian yang sesuai adalah sebagai berikut:

// calculator_test.go
package calculator

import "testing"

func TestAdd(t *testing.T) {
    expected := 4
    actual := add(2, 2)
    if actual != expected {
        t.Errorf("harapkan %d, dapatkan %d", expected, actual)
    }
}

Dengan meletakkan berkas pengujian dalam paket yang sama, kita dapat langsung menguji fungsi add.

6.2 Pola Pengujian Umum dan Praktik Terbaik

Pengujian unit Go memiliki beberapa pola umum yang memfasilitasi pekerjaan pengujian dan membantu menjaga kejelasan dan keberlanjutan kode.

  1. Pengujian Berbasis Tabel

    Pengujian berbasis tabel adalah metode pengorganisasian masukan pengujian dan keluaran yang diharapkan. Dengan mendefinisikan serangkaian kasus pengujian dan kemudian meloopingnya untuk pengujian, metode ini membuat sangat mudah untuk menambahkan kasus uji baru dan juga membuat kode lebih mudah dibaca dan dipelihara.

    // calculator_test.go
    package calculator

    import "testing"

    func TestAddTableDriven(t *testing.T) {
        var tests = []struct {
            a, b   int
            want   int
        }{
            {1, 2, 3},
            {2, 2, 4},
            {5, -1, 4},
        }

        for _, tt := range tests {
            testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
            t.Run(testname, func(t *testing.T) {
                ans := add(tt.a, tt.b)
                if tt.want != ans {
                    t.Errorf("dapatkan %d, harapkan %d", ans, tt.want)
                }
            })
        }
    }
  1. Menggunakan Mock untuk Pengujian

    Mocking adalah teknik pengujian yang melibatkan penggantian dependensi untuk menguji berbagai bagian fungsionalitas. Dalam Golang, antarmuka adalah cara utama untuk mengimplementasikan mocks. Dengan menggunakan antarmuka, implementasi mock dapat dibuat dan digunakan dalam pengujian.