Python

Go Testing: Unit Test & Benchmark

Panduan lengkap Go Testing โ€” testing package, table-driven tests, benchmarks, test coverage, mocking, HTTP testing, dan best practices untuk Go developer

1. โœ… Pengenalan Go Testing

Go memiliki testing package bawaan yang sangat powerful โ€” tidak perlu install framework pihak ketiga! Go testing mendukung unit test, benchmark, test coverage, fuzzing, dan examples.

Fitur Go Testing

Fitur Perintah Penjelasan
Unit Testgo testMenjalankan semua test function
Verbosego test -vOutput detail setiap test
Specific Testgo test -run TestNamaMenjalankan test tertentu
Benchmarkgo test -bench=.Mengukur performa fungsi
Coveragego test -coverPersentase kode yang ter-test
Race Detectorgo test -raceMendeteksi race condition
Fuzzinggo test -fuzz=FuzzXxxRandomized testing (Go 1.18+)
Parallelt.Parallel()Menjalankan test secara paralel

Conventions

Aturan Penjelasan
File naming*_test.go โ€” file test harus berakhiran _test.go
Function namingfunc TestXxx(t *testing.T) โ€” harus diawali Test + huruf kapital
PackageBisa package foo (white-box) atau package foo_test (black-box)
Same directoryFile test harus di directory yang sama dengan source code
Benchmark namingfunc BenchmarkXxx(b *testing.B)
Diagram: Struktur Test di Go
my-project/
โ”œโ”€โ”€ calculator.go           โ† Source code
โ”œโ”€โ”€ calculator_test.go      โ† Test file (sama package)
โ”œโ”€โ”€ math/
โ”‚   โ”œโ”€โ”€ math.go             โ† Source code
โ”‚   โ”œโ”€โ”€ math_test.go        โ† Unit test
โ”‚   โ”œโ”€โ”€ math_example_test.goโ† Example test
โ”‚   โ””โ”€โ”€ math_bench_test.go  โ† Benchmark test
โ””โ”€โ”€ api/
    โ”œโ”€โ”€ handler.go
    โ””โ”€โ”€ handler_test.go

// Jalankan semua test:
// go test ./...

// Jalankan test di package tertentu:
// go test ./math/

// Jalankan test dengan pattern:
// go test -run TestTambah ./...

2. Unit Test Dasar

Mari mulai dengan unit test sederhana. Buat file calculator.go dan calculator_test.go.

Source Code

Go โ€” calculator.go
package calculator

import "errors"

var ErrDivByZero = errors.New("pembagian dengan nol")
var ErrNegativeRoot = errors.New("akar dari bilangan negatif")

func Tambah(a, b float64) float64 {
    return a + b
}

func Kurang(a, b float64) float64 {
    return a - b
}

func Kali(a, b float64) float64 {
    return a * b
}

func Bagi(a, b float64) (float64, error) {
    if b == 0 {
        return 0, ErrDivByZero
    }
    return a / b, nil
}

func Faktorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * Faktorial(n-1)
}

func IsPrime(n int) bool {
    if n < 2 {
        return false
    }
    for i := 2; i*i <= n; i++ {
        if n%i == 0 {
            return false
        }
    }
    return true
}

Test File

Go โ€” calculator_test.go
package calculator

import (
    "math"
    "testing"
)

// Test sederhana
func TestTambah(t *testing.T) {
    result := Tambah(2, 3)
    expected := 5.0

    if result != expected {
        t.Errorf("Tambah(2, 3) = %f, expected %f", result, expected)
    }
}

// Test dengan tolerance untuk float
func TestTambahFloat(t *testing.T) {
    result := Tambah(0.1, 0.2)
    expected := 0.3

    if math.Abs(result-expected) > 1e-9 {
        t.Errorf("Tambah(0.1, 0.2) = %f, expected %f", result, expected)
    }
}

// Test error
func TestBagiByZero(t *testing.T) {
    _, err := Bagi(10, 0)
    if err == nil {
        t.Error("Bagi(10, 0) seharusnya mengembalikan error")
    }
    if err != ErrDivByZero {
        t.Errorf("Error = %v, expected %v", err, ErrDivByZero)
    }
}

// Test normal case
func TestBagiNormal(t *testing.T) {
    result, err := Bagi(10, 3)
    if err != nil {
        t.Fatalf("Tidak ada error yang diharapkan, got %v", err)
    }
    expected := 10.0 / 3.0
    if math.Abs(result-expected) > 1e-9 {
        t.Errorf("Bagi(10, 3) = %f, expected %f", result, expected)
    }
}

// Test function dengan banyak cases
func TestFaktorial(t *testing.T) {
    // Helper function untuk assertion
    assertEqual := func(t *testing.T, got, want int) {
        t.Helper() // Tandai sebagai helper (better error location)
        if got != want {
            t.Errorf("got %d, want %d", got, want)
        }
    }

    assertEqual(t, Faktorial(0), 1)
    assertEqual(t, Faktorial(1), 1)
    assertEqual(t, Faktorial(5), 120)
    assertEqual(t, Faktorial(10), 3628800)
}

// Test IsPrime
func TestIsPrime(t *testing.T) {
    primes := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29}
    nonPrimes := []int{0, 1, 4, 6, 8, 9, 10, 15, 21, 25}

    for _, n := range primes {
        if !IsPrime(n) {
            t.Errorf("IsPrime(%d) = false, expected true", n)
        }
    }
    for _, n := range nonPrimes {
        if IsPrime(n) {
            t.Errorf("IsPrime(%d) = true, expected false", n)
        }
    }
}

Menjalankan Test

Terminal
# Jalankan semua test di package saat ini
go test

# Verbose output
go test -v
# === RUN   TestTambah
# --- PASS: TestTambah (0.00s)
# === RUN   TestBagiByZero
# --- PASS: TestBagiByZero (0.00s)
# PASS
# ok  	calc	0.002s

# Jalankan test tertentu saja
go test -v -run TestTambah

# Jalankan semua test di semua package
go test ./...

# Dengan race detector
go test -race ./...

3. Table-Driven Tests

Table-driven tests adalah pattern testing paling populer di Go. Daripada menulis test terpisah untuk setiap case, kita mendefinisikan semua test cases dalam slice of struct.

Go โ€” Table-Driven Tests
package calculator

import (
    "math"
    "testing"
)

func TestTambahTableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     float64
        expected float64
    }{
        {"positif + positif", 2, 3, 5},
        {"positif + negatif", 5, -3, 2},
        {"negatif + negatif", -2, -3, -5},
        {"dengan nol", 5, 0, 5},
        {"nol + nol", 0, 0, 0},
        {"desimal", 1.5, 2.5, 4.0},
        {"angka besar", 1e10, 1e10, 2e10},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Tambah(tt.a, tt.b)
            if math.Abs(result-tt.expected) > 1e-9 {
                t.Errorf("Tambah(%f, %f) = %f, want %f",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

func TestBagiTableDriven(t *testing.T) {
    tests := []struct {
        name        string
        a, b        float64
        expected    float64
        expectError bool
        errType     error
    }{
        {"normal", 10, 2, 5, false, nil},
        {"hasil desimal", 10, 3, 3.333333, false, nil},
        {"negatif", -10, 2, -5, false, nil},
        {"bagi nol", 10, 0, 0, true, ErrDivByZero},
        {"nol dibagi", 0, 5, 0, false, nil},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Bagi(tt.a, tt.b)

            if tt.expectError {
                if err == nil {
                    t.Error("Expected error, got nil")
                }
                if err != tt.errType {
                    t.Errorf("Error type = %v, want %v", err, tt.errType)
                }
                return
            }

            if err != nil {
                t.Fatalf("Unexpected error: %v", err)
            }
            if math.Abs(result-tt.expected) > 1e-5 {
                t.Errorf("Bagi(%f, %f) = %f, want %f",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

func TestIsPrimeTableDriven(t *testing.T) {
    tests := []struct {
        input    int
        expected bool
    }{
        {0, false},
        {1, false},
        {2, true},
        {3, true},
        {4, false},
        {5, true},
        {15, false},
        {17, true},
        {49, false},
        {97, true},
        {-5, false},
    }

    for _, tt := range tests {
        t.Run(
            fmt.Sprintf("IsPrime(%d)", tt.input),
            func(t *testing.T) {
                got := IsPrime(tt.input)
                if got != tt.expected {
                    t.Errorf("IsPrime(%d) = %v, want %v",
                        tt.input, got, tt.expected)
                }
            },
        )
    }
}
๐Ÿ’ก Tips: t.Run() dan t.Helper()

Gunakan t.Run(name, func) untuk subtests yang bisa dijalankan/difilter secara terpisah. Gunakan t.Helper() di helper functions agar error message menunjukkan lokasi test yang sebenarnya, bukan lokasi helper function.

4. Subtests

t.Run() membuat subtests yang bisa dijalankan secara individual dan memiliki output yang terstruktur.

Go โ€” Subtests
package calculator

import "testing"

func TestKalkulasiLengkap(t *testing.T) {
    t.Run("Tambah", func(t *testing.T) {
        t.Run("positif", func(t *testing.T) {
            if Tambah(2, 3) != 5 {
                t.Error("gagal")
            }
        })
        t.Run("negatif", func(t *testing.T) {
            if Tambah(-2, -3) != -5 {
                t.Error("gagal")
            }
        })
    })

    t.Run("Bagi", func(t *testing.T) {
        t.Run("normal", func(t *testing.T) {
            result, err := Bagi(10, 2)
            if err != nil {
                t.Fatal(err)
            }
            if result != 5 {
                t.Errorf("got %f, want 5", result)
            }
        })
        t.Run("error_nol", func(t *testing.T) {
            _, err := Bagi(10, 0)
            if err == nil {
                t.Error("expected error")
            }
        })
    })
}

// Jalankan subtest tertentu:
// go test -run TestKalkulasiLengkap/Tambah/positif
// go test -run TestKalkulasiLengkap/Bagi

// Parallel subtests
func TestParallelSubtests(t *testing.T) {
    tests := []struct {
        name  string
        input int
    }{
        {"case_1", 1},
        {"case_2", 2},
        {"case_3", 3},
    }

    for _, tt := range tests {
        tt := tt // capture loop variable
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // Jalankan secara paralel
            result := Faktorial(tt.input)
            if result <= 0 {
                t.Error("Hasil harus positif")
            }
        })
    }
}

5. Test Fixtures & TestMain

TestMain dijalankan sekali sebelum semua test di package โ€” cocok untuk setup/teardown seperti database connection atau test fixtures.

Go โ€” TestMain & Fixtures
package calculator

import (
    "fmt"
    "os"
    "testing"
}

// TestMain โ€” dijalankan sekali sebelum semua test
func TestMain(m *testing.M) {
    fmt.Println("๐Ÿ”ง Setup: Inisialisasi test environment...")

    // Setup (misal: buat database test, load fixtures)
    setup()

    // Jalankan semua test
    code := m.Run()

    // Teardown (misal: hapus database test)
    teardown()

    fmt.Println("๐Ÿงน Teardown: Cleanup selesai")
    os.Exit(code)
}

func setup() {
    fmt.Println("  โœ… Database test siap")
    fmt.Println("  โœ… Fixtures loaded")
}

func teardown() {
    fmt.Println("  ๐Ÿ—‘๏ธ Database test dihapus")
}

// Helper functions untuk test
func assertFloat(t *testing.T, got, want float64) {
    t.Helper()
    diff := got - want
    if diff < 0 {
        diff = -diff
    }
    if diff > 1e-9 {
        t.Errorf("got %f, want %f", got, want)
    }
}

func assertError(t *testing.T, got, want error) {
    t.Helper()
    if got != want {
        t.Errorf("got error %v, want %v", got, want)
    }
}

func assertNoError(t *testing.T, err error) {
    t.Helper()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}

func assertTrue(t *testing.T, val bool, msg string) {
    t.Helper()
    if !val {
        t.Error(msg)
    }
}

// Temp files dan directories
func TestWithTempFile(t *testing.T) {
    // t.TempDir() โ€” otomatis dihapus setelah test selesai
    dir := t.TempDir()
    fmt.Println("Temp dir:", dir)

    // Buat file di temp directory
    filePath := dir + "/test.txt"
    os.WriteFile(filePath, []byte("test data"), 0644)

    // File akan otomatis dihapus setelah test selesai
}

6. Mocking & Dependency Injection

Go menggunakan interface untuk mocking โ€” tidak perlu framework mocking. Ini adalah salah satu keunggulan desain Go: dependency injection melalui interface.

Go โ€” Mocking dengan Interface
// === Source code ===
// user_service.go
package user

// Interface untuk dependency
type UserRepository interface {
    GetByID(id int) (*User, error)
    Save(user *User) error
    Delete(id int) error
}

type User struct {
    ID    int
    Name  string
    Email string
}

// Service menggunakan interface (bukan implementasi konkret)
type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) GetUserName(id int) (string, error) {
    user, err := s.repo.GetByID(id)
    if err != nil {
        return "", err
    }
    return user.Name, nil
}

func (s *UserService) Register(name, email string) (*User, error) {
    // Validasi
    if name == "" {
        return nil, errors.New("nama tidak boleh kosong")
    }
    if !strings.Contains(email, "@") {
        return nil, errors.New("email tidak valid")
    }

    user := &User{Name: name, Email: email}
    if err := s.repo.Save(user); err != nil {
        return nil, err
    }
    return user, nil
}
Go โ€” Mock Implementation & Test
// === Test file ===
// user_service_test.go
package user

import (
    "errors"
    "testing"
)

// Mock repository
type MockUserRepo struct {
    users   map[int]*User
    saveErr error
    getErr  error
}

func NewMockRepo() *MockUserRepo {
    return &MockUserRepo{
        users: map[int]*User{
            1: {ID: 1, Name: "Budi", Email: "budi@mail.com"},
            2: {ID: 2, Name: "Ani", Email: "ani@mail.com"},
        },
    }
}

func (m *MockUserRepo) GetByID(id int) (*User, error) {
    if m.getErr != nil {
        return nil, m.getErr
    }
    user, ok := m.users[id]
    if !ok {
        return nil, errors.New("user not found")
    }
    return user, nil
}

func (m *MockUserRepo) Save(user *User) error {
    if m.saveErr != nil {
        return m.saveErr
    }
    user.ID = len(m.users) + 1
    m.users[user.ID] = user
    return nil
}

func (m *MockUserRepo) Delete(id int) error {
    delete(m.users, id)
    return nil
}

// === Tests ===

func TestGetUserName(t *testing.T) {
    t.Run("success", func(t *testing.T) {
        mock := NewMockRepo()
        svc := NewUserService(mock)

        name, err := svc.GetUserName(1)
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
        if name != "Budi" {
            t.Errorf("got %s, want Budi", name)
        }
    })

    t.Run("not_found", func(t *testing.T) {
        mock := NewMockRepo()
        svc := NewUserService(mock)

        _, err := svc.GetUserName(999)
        if err == nil {
            t.Error("expected error")
        }
    })

    t.Run("repo_error", func(t *testing.T) {
        mock := NewMockRepo()
        mock.getErr = errors.New("database connection failed")
        svc := NewUserService(mock)

        _, err := svc.GetUserName(1)
        if err == nil {
            t.Error("expected error")
        }
    })
}

func TestRegister(t *testing.T) {
    tests := []struct {
        name        string
        userName    string
        email       string
        saveErr     error
        expectError bool
    }{
        {"success", "Dimas", "dimas@mail.com", nil, false},
        {"empty_name", "", "test@mail.com", nil, true},
        {"invalid_email", "Sari", "invalid-email", nil, true},
        {"save_error", "Rina", "rina@mail.com", errors.New("db error"), true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mock := NewMockRepo()
            mock.saveErr = tt.saveErr
            svc := NewUserService(mock)

            user, err := svc.Register(tt.userName, tt.email)
            if tt.expectError {
                if err == nil {
                    t.Error("expected error")
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if user.Name != tt.userName {
                t.Errorf("name = %s, want %s", user.Name, tt.userName)
            }
        })
    }
}

7. HTTP Handler Testing

Go menyediakan httptest package untuk menguji HTTP handler tanpa perlu menjalankan server sebenarnya.

Go โ€” HTTP Handler Testing
// === Source: handler.go ===
package api

import (
    "encoding/json"
    "net/http"
)

type Response struct {
    Status  string      `json:"status"`
    Data    interface{} `json:"data,omitempty"`
    Error   string      `json:"error,omitempty"`
}

func HealthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(Response{Status: "ok"})
}

func GetUserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    if id == "" {
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(Response{Error: "ID diperlukan"})
        return
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(Response{
        Status: "ok",
        Data:   map[string]string{"id": id, "name": "Budi"},
    })
}
Go โ€” HTTP Test
// === Test: handler_test.go ===
package api

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHealthHandler(t *testing.T) {
    // Buat request
    req := httptest.NewRequest(http.MethodGet, "/health", nil)

    // Buat ResponseRecorder
    w := httptest.NewRecorder()

    // Panggil handler
    HealthHandler(w, req)

    // Assert status code
    if w.Code != http.StatusOK {
        t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
    }

    // Parse response
    var resp Response
    json.NewDecoder(w.Body).Decode(&resp)

    if resp.Status != "ok" {
        t.Errorf("status = %s, want ok", resp.Status)
    }
}

func TestGetUserHandler(t *testing.T) {
    tests := []struct {
        name       string
        path       string
        wantStatus int
        wantName   string
    }{
        {"valid_id", "/api/users/1", http.StatusOK, "Budi"},
        {"another_id", "/api/users/2", http.StatusOK, "Budi"},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest(http.MethodGet, tt.path, nil)
            req.SetPathValue("id", tt.path[len("/api/users/"):])

            w := httptest.NewRecorder()
            GetUserHandler(w, req)

            if w.Code != tt.wantStatus {
                t.Errorf("status = %d, want %d", w.Code, tt.wantStatus)
            }

            var resp Response
            json.NewDecoder(w.Body).Decode(&resp)

            if data, ok := resp.Data.(map[string]interface{}); ok {
                if name, ok := data["name"].(string); ok {
                    if name != tt.wantName {
                        t.Errorf("name = %s, want %s", name, tt.wantName)
                    }
                }
            }
        })
    }
}

// Test dengan Gin (jika pakai Gin)
// func TestGinHandler(t *testing.T) {
//     gin.SetMode(gin.TestMode)
//     r := gin.New()
//     r.GET("/health", func(c *gin.Context) {
//         c.JSON(200, gin.H{"status": "ok"})
//     })
//
//     req := httptest.NewRequest("GET", "/health", nil)
//     w := httptest.NewRecorder()
//     r.ServeHTTP(w, req)
//
//     if w.Code != 200 {
//         t.Errorf("expected 200, got %d", w.Code)
//     }
// }

8. Benchmarking

Benchmark mengukur performa fungsi dalam nanoseconds per operation. Sangat penting untuk mengoptimasi kode kritis.

Go โ€” Benchmark
package calculator

import (
    "fmt"
    "strings"
    "testing"
)

// Benchmark sederhana
func BenchmarkTambah(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Tambah(2.5, 3.7)
    }
}

func BenchmarkKali(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Kali(2.5, 3.7)
    }
}

func BenchmarkFaktorial(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Faktorial(20)
    }
}

func BenchmarkIsPrime(b *testing.B) {
    for i := 0; i < b.N; i++ {
        IsPrime(97)
    }
}

// Benchmark dengan data berbeda
func BenchmarkFaktorialSizes(b *testing.B) {
    sizes := []int{5, 10, 20, 50}

    for _, size := range sizes {
        b.Run(fmt.Sprintf("n=%d", size), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                Faktorial(size)
            }
        })
    }
}

// Benchmark string concatenation methods
func BenchmarkStringConcat(b *testing.B) {
    b.Run("Plus", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            s := ""
            for j := 0; j < 100; j++ {
                s += "x"
            }
        }
    })

    b.Run("Builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            for j := 0; j < 100; j++ {
                sb.WriteString("x")
            }
            _ = sb.String()
        }
    })

    b.Run("Join", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            parts := make([]string, 100)
            for j := 0; j < 100; j++ {
                parts[j] = "x"
            }
            _ = strings.Join(parts, "")
        }
    })
}

// Benchmark map operations
func BenchmarkMapVsSlice(b *testing.B) {
    b.Run("Map_Lookup", func(b *testing.B) {
        m := make(map[int]bool)
        for i := 0; i < 1000; i++ {
            m[i] = true
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            _ = m[500]
        }
    })

    b.Run("Slice_Search", func(b *testing.B) {
        s := make([]int, 1000)
        for i := 0; i < 1000; i++ {
            s[i] = i
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            for _, v := range s {
                if v == 500 {
                    break
                }
            }
        }
    })
}

Menjalankan Benchmark

Terminal
# Jalankan semua benchmark
go test -bench=.

# Verbose output
go test -bench=. -benchmem
# BenchmarkTambah-8    1000000000    0.25 ns/op    0 B/op    0 allocs/op
# BenchmarkFaktorial-8  20000000    58.5 ns/op     0 B/op    0 allocs/op

# Jalankan benchmark tertentu
go test -bench=BenchmarkTambah -benchmem

# Set jumlah iterasi (count)
go test -bench=. -count=5

# Memory profiling
go test -bench=. -benchmem -memprofile=mem.out
go tool pprof mem.out

# CPU profiling
go test -bench=. -cpuprofile=cpu.out
go tool pprof cpu.out

# Output penjelasan:
# BenchmarkXxx-8   1000000000   0.25 ns/op   0 B/op   0 allocs/op
#  โ”‚       โ”‚          โ”‚           โ”‚             โ”‚         โ”‚
#  โ”‚       โ”‚          โ”‚           โ”‚             โ”‚         โ””โ”€ allocs per op
#  โ”‚       โ”‚          โ”‚           โ”‚             โ””โ”€ bytes per op
#  โ”‚       โ”‚          โ”‚           โ””โ”€ nanoseconds per operation
#  โ”‚       โ”‚          โ””โ”€ total iterations
#  โ”‚       โ””โ”€ GOMAXPROCS
#  โ””โ”€ benchmark name

9. Test Coverage

Test coverage menunjukkan persentase kode yang dijalankan oleh test. Targetkan minimal 70-80% untuk production code.

Terminal โ€” Test Coverage
# Lihat coverage percentage
go test -cover
# PASS
# coverage: 85.7% of statements
# ok  calculator  0.003s

# Coverage untuk semua package
go test -cover ./...

# Generate coverage profile
go test -coverprofile=coverage.out

# Lihat coverage di browser (HTML report)
go tool cover -html=coverage.out

# Lihat detail per function
go test -coverprofile=coverage.out
go tool cover -func=coverage.out
# calculator.go:12:   Tambah     100.0%
# calculator.go:16:   Kurang     100.0%
# calculator.go:20:   Kali       100.0%
# calculator.go:24:   Bagi        85.7%
# calculator.go:32:   Faktorial   100.0%
# calculator.go:38:   IsPrime      66.7%
# total:              (statements) 87.5%

# Coverage dengan race detector
go test -cover -race ./...

# Coverage threshold (script)
# Cek apakah coverage di atas 80%
coverage=$(go test -cover ./... | grep -oP 'coverage: \K[0-9.]+')
if (( $(echo "$coverage < 80" | bc -l) )); then
    echo "โŒ Coverage $coverage% di bawah threshold 80%"
    exit 1
fi
echo "โœ… Coverage $coverage%"
โš ๏ธ Coverage Bukan Segalanya!

Coverage 100% tidak menjamin kode bebas bug. Yang lebih penting adalah kualitas test โ€” apakah test menguji edge cases, error handling, dan behavior yang benar? Fokus pada test yang bermakna, bukan sekadar angka coverage.

10. Best Practices & Tips

Go โ€” Testing Best Practices
// 1. Gunakan t.Helper() di helper functions
func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

// 2. Gunakan t.Fatal() untuk error yang menghentikan test
func TestSomething(t *testing.T) {
    db, err := connectDB()
    if err != nil {
        t.Fatalf("Cannot connect to DB: %v", err) // Stop test
    }
    defer db.Close()
    // ... lanjut test
}

// 3. Gunakan t.Cleanup() untuk teardown
func TestWithCleanup(t *testing.T) {
    dir := t.TempDir()
    file := createTempFile(dir)

    t.Cleanup(func() {
        // Otomatis dijalankan setelah test selesai
        fmt.Println("Cleanup:", dir)
    })

    // Test logic di sini...
}

// 4. Test error messages yang deskriptif
// โŒ Buruk:
if result != expected {
    t.Error("wrong result")
}

// โœ… Bagus:
if result != expected {
    t.Errorf("Tambah(2, 3) = %f; want %f", result, expected)
}

// 5. Gunakan t.Parallel() untuk test independen
func TestParallel(t *testing.T) {
    t.Parallel()
    // Test ini bisa berjalan paralel dengan test lain
}

// 6. Jangan test implementation details โ€” test behavior
// โŒ Buruk: Mengecek internal state
// โœ… Bagus: Mengecek output dan behavior

// 7. Setiap test harus independen dan bisa dijalankan sendiri
// โŒ Buruk: Test B bergantung pada test A
// โœ… Bagus: Setiap test membuat state sendiri

// 8. Gunakan t.Skip() untuk conditional tests
func TestNeedsDatabase(t *testing.T) {
    if os.Getenv("DATABASE_URL") == "" {
        t.Skip("DATABASE_URL not set, skipping")
    }
    // ... test database
}

// 9. Example functions โ€” auto-generated docs + test
func ExampleTambah() {
    fmt.Println(Tambah(2, 3))
    // Output: 5
}

func ExampleBagi() {
    result, _ := Bagi(10, 3)
    fmt.Printf("%.2f\n", result)
    // Output: 3.33
}

// 10. Fuzzing โ€” randomized testing (Go 1.18+)
func FuzzIsPrime(f *testing.F) {
    f.Add(2)   // Seed corpus
    f.Add(15)
    f.Add(97)

    f.Fuzz(func(t *testing.T, n int) {
        result := IsPrime(n)
        // Tidak boleh panic, regardless of input
        if n < 2 && result {
            t.Errorf("IsPrime(%d) = true, want false for n < 2", n)
        }
    })
}
// Jalankan: go test -fuzz=FuzzIsPrime -fuzztime=30s

11. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Go Testing:

Pertanyaan 1: Apa aturan penamaan file test di Go?

a) File test harus diakhiri dengan .test
b) File test harus diakhiri dengan _test.go
c) File test harus diawali dengan test_
d) Nama file test bebas

Pertanyaan 2: Apa itu table-driven tests di Go?

a) Test yang hanya untuk database
b) Test dengan mendefinisikan semua test cases dalam slice of struct
c) Test yang dijalankan di tabel database
d) Test yang menggunakan spreadsheet

Pertanyaan 3: Perintah apa yang digunakan untuk menjalankan benchmark?

a) go test -speed
b) go bench .
c) go test -bench=.
d) go test -performance

Pertanyaan 4: Perbedaan antara t.Error() dan t.Fatal()?

a) Tidak ada perbedaan
b) t.Error() menghentikan test, t.Fatal() melanjutkan
c) t.Error() melanjutkan test, t.Fatal() menghentikan test segera
d) t.Fatal() hanya untuk panic

Pertanyaan 5: Bagaimana cara mocking dependency di Go?

a) Menggunakan framework mock seperti Mockito
b) Menggunakan interface dan mock implementation
c) Mengubah source code secara langsung
d) Menggunakan @mock annotation
๐Ÿ” Zoom
100%
๐ŸŽจ Tema