Build CoreOrdered learning track

Benchmarking, Fuzzing, dan Property-style Thinking

Materi mendalam tentang benchmark, fuzzing, dan property-style thinking di Go: b.N, allocation measurement, fuzz target, seed corpus, invariants, dan regression testing.

11 min read2058 words
PrevNext
Lesson 1235 lesson track0719 Build Core
#go#golang#benchmarking#fuzzing+3 more

Benchmarking, Fuzzing, dan Property-style Thinking

Target part ini: kamu mampu memakai benchmark untuk mengukur cost, fuzzing untuk menemukan edge case, dan property-style thinking untuk mendesain test berdasarkan invariant, bukan hanya contoh input-output.

Unit test menjawab:

Apakah contoh behavior yang kita pilih sudah benar?

Benchmark menjawab:

Berapa cost behavior ini?

Fuzzing menjawab:

Apakah ada input yang belum kita bayangkan yang bisa merusak behavior ini?

Property-style thinking menjawab:

Invariant apa yang harus selalu benar untuk semua input valid?

Keempatnya saling melengkapi.

Dalam framework Kaufman, benchmark dan fuzzing memperkuat feedback loop. Kamu tidak hanya berlatih menulis kode, tetapi belajar membaca sinyal: correctness, cost, edge case, dan regression.


1. Benchmark Bukan Tebakan Performa

Engineer sering salah menebak bottleneck.

Contoh tebakan umum:

  • “String concatenation pasti lambat.”
  • “Pointer pasti lebih cepat dari value.”
  • “Goroutine pasti mempercepat.”
  • “Map pasti cukup cepat.”
  • “JSON decode bukan masalah.”

Kadang benar. Sering salah konteks.

Benchmark membuat diskusi lebih konkret:

Before: menurut saya lebih cepat
After : benchmark menunjukkan 40 ns/op vs 120 ns/op, 0 allocs/op vs 2 allocs/op

Tetapi benchmark juga bisa menipu jika salah desain.

Tujuan part ini bukan membuat micro-benchmark sembarangan, melainkan membangun disiplin:

measure -> understand -> change -> measure again

2. Bentuk Benchmark Go

Benchmark ditulis di file _test.go.

Signature:

func BenchmarkName(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // code being measured
    }
}

Contoh:

func BenchmarkNormalizeEmail(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = NormalizeEmail("  USER@EXAMPLE.COM  ")
    }
}

Jalankan:

go test ./... -bench=.

Contoh output:

BenchmarkNormalizeEmail-8    12849382    92.3 ns/op

Artinya:

  • benchmark bernama BenchmarkNormalizeEmail,
  • berjalan dengan parallelism/runtime tertentu,
  • loop dieksekusi sekitar 12,849,382 iterasi,
  • rata-rata 92.3 nanosecond per operation.

b.N ditentukan oleh test runner. Jangan set manual.


3. Benchmark dengan Allocation Report

Allocation sering penting di Go karena allocation bisa meningkatkan tekanan GC.

Jalankan:

go test ./... -bench=. -benchmem

Output:

BenchmarkNormalizeEmail-8    12849382    92.3 ns/op    16 B/op    1 allocs/op

Kolom penting:

KolomMakna
ns/opwaktu rata-rata per operation
B/opbyte allocation per operation
allocs/opjumlah allocation per operation

Allocation bukan selalu buruk. Tetapi allocation yang tidak perlu dalam hot path bisa berdampak pada latency dan throughput.

Rule:

Optimize allocation only after knowing path tersebut penting.

4. Benchmark Harus Menghindari Compiler Elimination

Compiler bisa menghapus kerja yang hasilnya tidak dipakai.

Buruk:

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}

Kalau compiler bisa membuktikan hasil tidak dipakai, benchmark bisa tidak mengukur apa yang kamu kira.

Lebih aman:

var sink int

func BenchmarkAdd(b *testing.B) {
    var result int
    for i := 0; i < b.N; i++ {
        result = Add(1, 2)
    }
    sink = result
}

Untuk object:

var userSink User

func BenchmarkParseUser(b *testing.B) {
    data := []byte(`{"id":"u1","email":"ada@example.com"}`)

    var user User
    for i := 0; i < b.N; i++ {
        user, _ = ParseUser(data)
    }
    userSink = user
}

Gunakan sink hanya jika perlu. Jangan membuat benchmark lebih kompleks dari yang dibutuhkan.


5. Setup Cost vs Measured Cost

Kesalahan umum: setup ikut terukur.

Buruk:

func BenchmarkParseUser(b *testing.B) {
    for i := 0; i < b.N; i++ {
        data := []byte(`{"id":"u1","email":"ada@example.com"}`)
        _, _ = ParseUser(data)
    }
}

Benchmark ini mengukur alokasi data juga, bukan hanya parsing.

Lebih baik:

func BenchmarkParseUser(b *testing.B) {
    data := []byte(`{"id":"u1","email":"ada@example.com"}`)

    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        _, _ = ParseUser(data)
    }
}

b.ResetTimer() mengabaikan waktu setup sebelum loop.

Jika setup per iteration memang bagian dari workload nyata, boleh dimasukkan. Yang penting sadar apa yang diukur.


6. b.ReportAllocs

Selain -benchmem, kamu bisa memanggil:

func BenchmarkParseUser(b *testing.B) {
    b.ReportAllocs()

    data := []byte(`{"id":"u1","email":"ada@example.com"}`)
    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        _, _ = ParseUser(data)
    }
}

Ini membuat benchmark melaporkan allocation saat benchmark dijalankan.


7. Sub-benchmark

Sub-benchmark mirip subtest.

func BenchmarkNormalizeEmail(b *testing.B) {
    cases := []struct {
        name  string
        input string
    }{
        {"already_normal", "user@example.com"},
        {"upper_case", "USER@EXAMPLE.COM"},
        {"with_spaces", "  USER@EXAMPLE.COM  "},
    }

    for _, tc := range cases {
        b.Run(tc.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                _ = NormalizeEmail(tc.input)
            }
        })
    }
}

Output:

BenchmarkNormalizeEmail/already_normal-8
BenchmarkNormalizeEmail/upper_case-8
BenchmarkNormalizeEmail/with_spaces-8

Sub-benchmark cocok untuk membandingkan:

  • ukuran input,
  • algoritma,
  • codec,
  • mode konfigurasi,
  • data shape,
  • implementation lama vs baru.

8. Benchmark Ukuran Input

Bottleneck sering baru muncul pada ukuran tertentu.

func BenchmarkDeduplicateStrings(b *testing.B) {
    sizes := []int{10, 100, 1000, 10000}

    for _, size := range sizes {
        b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
            input := makeStrings(size)
            b.ResetTimer()

            for i := 0; i < b.N; i++ {
                _ = DeduplicateStrings(input)
            }
        })
    }
}

Hasilnya bisa menunjukkan kompleksitas praktis.

Misalnya:

size_10       200 ns/op
size_100      2 us/op
size_1000     35 us/op
size_10000    5 ms/op

Ini membantu membedakan:

  • cost konstan,
  • cost linear,
  • cost quadratic,
  • cost akibat allocation,
  • cost akibat hashing,
  • cost akibat sorting.

9. Membandingkan Dua Implementasi

Misal ada dua implementation:

func JoinSlow(parts []string) string {
    var out string
    for _, p := range parts {
        out += p
    }
    return out
}

func JoinBuilder(parts []string) string {
    var b strings.Builder
    for _, p := range parts {
        b.WriteString(p)
    }
    return b.String()
}

Benchmark:

var stringSink string

func BenchmarkJoin(b *testing.B) {
    parts := []string{"a", "b", "c", "d", "e", "f", "g", "h"}

    b.Run("slow", func(b *testing.B) {
        var s string
        for i := 0; i < b.N; i++ {
            s = JoinSlow(parts)
        }
        stringSink = s
    })

    b.Run("builder", func(b *testing.B) {
        var s string
        for i := 0; i < b.N; i++ {
            s = JoinBuilder(parts)
        }
        stringSink = s
    })
}

Benchmark membantu, tetapi jangan berhenti di angka. Tanyakan:

  • apakah workload representatif?
  • apakah input size realistis?
  • apakah readability memburuk?
  • apakah improvement signifikan untuk path penting?
  • apakah allocation berkurang?
  • apakah complexity bertambah?

10. Benchstat untuk Membandingkan Perubahan

Untuk perubahan serius, gunakan beberapa run.

go test ./... -bench=. -benchmem -count=10 > old.txt
# ubah implementation
go test ./... -bench=. -benchmem -count=10 > new.txt
benchstat old.txt new.txt

benchstat membantu membandingkan distribusi hasil, bukan satu angka tunggal.

Satu run benchmark bisa noise karena:

  • CPU scheduling,
  • background process,
  • thermal throttling,
  • power mode,
  • VM/container noise,
  • GC timing,
  • turbo boost.

Rule:

Never make a serious performance claim from one benchmark run.

11. Benchmark Smells

11.1 Mengukur hal yang tidak relevan

Benchmark micro-operation yang bukan bottleneck production.

11.2 Workload tidak realistis

Input terlalu kecil, terlalu bersih, atau tidak mencerminkan data nyata.

11.3 Setup ikut terukur tanpa sengaja

Gunakan b.ResetTimer().

11.4 Hasil tidak dipakai

Compiler bisa menghilangkan computation.

11.5 Benchmark menyebabkan shared state pollution

State berubah antar iteration sehingga hasil makin lama makin beda.

11.6 Membandingkan readability buruk dengan improvement kecil

Jika improvement 1% tetapi code menjadi opaque, belum tentu layak.

11.7 Tidak mengukur allocation

Untuk Go, allocs/op sering sama pentingnya dengan ns/op.


12. Fuzzing: Menemukan Input yang Tidak Kamu Pikirkan

Fuzzing adalah testing otomatis yang memanipulasi input untuk mencari bug. Di Go, fuzzing native terintegrasi dengan go test.

Fuzzing sangat berguna untuk code yang menerima input tidak sepenuhnya terpercaya:

  • parser,
  • decoder,
  • validator,
  • URL handling,
  • path handling,
  • serializer/deserializer,
  • token parser,
  • compression/decompression,
  • protocol handling,
  • template rendering,
  • boundary security.

Unit test menguji contoh yang kamu pilih. Fuzzing mencari contoh yang kamu tidak pilih.


13. Bentuk Fuzz Test Go

Signature:

func FuzzName(f *testing.F) {
    f.Add(seed1)
    f.Add(seed2)

    f.Fuzz(func(t *testing.T, input Type) {
        // fuzz target
    })
}

Contoh:

func FuzzParseUserID(f *testing.F) {
    f.Add("user_123")
    f.Add("")
    f.Add("../admin")
    f.Add("こんにちは")

    f.Fuzz(func(t *testing.T, raw string) {
        id, err := ParseUserID(raw)
        if err != nil {
            return
        }

        if string(id) == "" {
            t.Fatalf("ParseUserID(%q) returned empty id without error", raw)
        }
    })
}

Jalankan seed corpus sebagai test biasa:

go test ./...

Jalankan fuzzing:

go test ./... -fuzz=FuzzParseUserID

Batasi durasi:

go test ./... -fuzz=FuzzParseUserID -fuzztime=30s

14. Fuzzing Bukan Random Assertion Sembarangan

Fuzz test yang buruk:

func FuzzParse(f *testing.F) {
    f.Fuzz(func(t *testing.T, s string) {
        Parse(s)
    })
}

Ini hanya memastikan tidak panic, dan itu pun mungkin terlalu lemah.

Lebih baik cari invariant.

Contoh invariant parser:

Jika Parse sukses, hasilnya harus memenuhi Valid().
func FuzzParseUserID(f *testing.F) {
    f.Add("user_123")

    f.Fuzz(func(t *testing.T, raw string) {
        id, err := ParseUserID(raw)
        if err != nil {
            return
        }

        if !id.Valid() {
            t.Fatalf("ParseUserID(%q) returned invalid ID %q", raw, id)
        }
    })
}

Contoh invariant round-trip:

decode(encode(x)) == x
func FuzzJSONRoundTrip(f *testing.F) {
    f.Add("ada@example.com", int64(42))

    f.Fuzz(func(t *testing.T, email string, balance int64) {
        if balance < 0 {
            return
        }

        in := User{Email: email, Balance: balance}

        data, err := EncodeUser(in)
        if err != nil {
            t.Fatalf("EncodeUser() error = %v", err)
        }

        out, err := DecodeUser(data)
        if err != nil {
            t.Fatalf("DecodeUser(EncodeUser(x)) error = %v", err)
        }

        if out != in {
            t.Fatalf("round trip mismatch: got %#v, want %#v", out, in)
        }
    })
}

15. Property-style Thinking

Property-style thinking berarti mendesain test dari invariant umum.

Bukan:

Untuk input A, output B.

Tetapi:

Untuk semua input yang memenuhi constraint X, property Y harus selalu benar.

Contoh property:

DomainProperty
Normalize emailhasil tidak punya leading/trailing spaces
Sortoutput terurut dan elemen sama dengan input
Encode/decodedecode(encode(x)) == x
Compress/decompressdecompress(compress(x)) == x
Parserjika sukses, output valid
Sanitizeroutput tidak mengandung karakter terlarang
Money transfertotal balance tidak berubah
Deduplicationoutput tidak punya duplicate dan semua berasal dari input
Paginationtidak ada item hilang/duplikat antar page
State transitiontransition invalid tidak mengubah state

Fuzzing paling kuat saat kamu punya property yang jelas.


16. Contoh Property: Sort

Function:

func SortInts(xs []int) []int {
    ys := append([]int(nil), xs...)
    sort.Ints(ys)
    return ys
}

Property:

  1. output sorted,
  2. output punya panjang sama,
  3. output punya multiset elemen yang sama,
  4. input tidak termutasi.

Fuzz:

func FuzzSortInts(f *testing.F) {
    f.Add([]byte{3, 1, 2})
    f.Add([]byte{})
    f.Add([]byte{255, 0, 255})

    f.Fuzz(func(t *testing.T, data []byte) {
        input := make([]int, len(data))
        for i, b := range data {
            input[i] = int(b)
        }

        original := append([]int(nil), input...)
        got := SortInts(input)

        if !sort.IntsAreSorted(got) {
            t.Fatalf("output is not sorted: %v", got)
        }
        if len(got) != len(input) {
            t.Fatalf("len(got) = %d, want %d", len(got), len(input))
        }
        if !sameMultiset(got, original) {
            t.Fatalf("output elements differ: got %v, input %v", got, original)
        }
        if !slices.Equal(input, original) {
            t.Fatalf("input mutated: got %v, want %v", input, original)
        }
    })
}

Helper:

func sameMultiset(a, b []int) bool {
    if len(a) != len(b) {
        return false
    }

    counts := make(map[int]int, len(a))
    for _, x := range a {
        counts[x]++
    }
    for _, x := range b {
        counts[x]--
        if counts[x] < 0 {
            return false
        }
    }
    return true
}

Perhatikan: kita tidak menyebut expected output untuk setiap input. Kita menyebut sifat yang selalu benar.


17. Fuzz Seed Corpus

f.Add menambahkan seed input.

Seed yang baik mencakup:

  • empty input,
  • minimal valid input,
  • typical valid input,
  • maximum-ish input,
  • unicode,
  • invalid syntax,
  • reserved characters,
  • path traversal pattern,
  • SQL-like string,
  • repeated delimiters,
  • nested structure,
  • very long string jika relevan.

Contoh:

func FuzzNormalizePath(f *testing.F) {
    seeds := []string{
        "",
        "/",
        "/users/ada",
        "../admin",
        "/users//ada",
        "/users/%2e%2e/admin",
        "こんにちは",
        strings.Repeat("a", 1024),
    }

    for _, seed := range seeds {
        f.Add(seed)
    }

    f.Fuzz(func(t *testing.T, path string) {
        got, err := NormalizePath(path)
        if err != nil {
            return
        }
        if strings.Contains(got, "..") {
            t.Fatalf("NormalizePath(%q) = %q contains traversal", path, got)
        }
    })
}

Seed bukan exhaustive list. Seed adalah starting point untuk mutation engine.


18. Regression dari Fuzz Failure

Saat fuzzing menemukan failure, Go menyimpan input yang memicu failure agar bisa dijalankan ulang sebagai regression.

Workflow:

  1. Jalankan fuzz.
  2. Fuzz menemukan input gagal.
  3. Baca failing input.
  4. Reproduce dengan go test.
  5. Perbaiki bug.
  6. Pastikan input gagal menjadi regression corpus.

Mental model:

fuzz failure is not noise; it is a new test case discovered by the machine.

Jangan hanya mengubah fuzz test agar failure hilang. Klasifikasikan dulu:

  • Apakah input valid?
  • Kalau valid, apakah code salah?
  • Kalau invalid, apakah function harus return error, bukan panic?
  • Apakah property test terlalu kuat?
  • Apakah expectation salah?
  • Apakah ada vulnerability?

19. Fuzzing Security-sensitive Code

Fuzzing sangat relevan untuk security boundary.

Target bagus:

  • request parser,
  • token parser,
  • authorization expression parser,
  • file path normalizer,
  • template renderer,
  • markdown/html sanitizer,
  • JSON schema validator,
  • custom query language,
  • import/export processor,
  • webhook verifier,
  • compression parser,
  • binary protocol decoder.

Cari property seperti:

No panic for any input.
Invalid input returns error.
Successful parse produces valid object.
Normalizer never returns path outside allowed root.
Sanitizer never emits unsafe tag.
Decoder never allocates beyond configured limit.

Contoh path safety:

func FuzzSafeJoin(f *testing.F) {
    f.Add("users/ada.txt")
    f.Add("../secret.txt")
    f.Add("/etc/passwd")

    root := "/safe/root"

    f.Fuzz(func(t *testing.T, name string) {
        got, err := SafeJoin(root, name)
        if err != nil {
            return
        }

        cleanRoot := filepath.Clean(root)
        cleanGot := filepath.Clean(got)

        if cleanGot != cleanRoot && !strings.HasPrefix(cleanGot, cleanRoot+string(os.PathSeparator)) {
            t.Fatalf("SafeJoin(%q, %q) escaped root: %q", root, name, got)
        }
    })
}

Fuzzing tidak membuktikan aman, tetapi memperbesar peluang menemukan edge case berbahaya.


20. Fuzzing Smells

20.1 Fuzz target punya side effect eksternal

Jangan fuzz function yang menulis database production, memanggil network eksternal, atau mengubah file global.

20.2 Fuzz terlalu lambat

Fuzz target dipanggil sangat banyak. Jaga agar cepat.

20.3 Property tidak jelas

Kalau test hanya memanggil function tanpa assertion, sinyalnya lemah.

20.4 Panic dipakai untuk invalid input

Untuk input dari luar, invalid input seharusnya error, bukan panic.

20.5 Fuzz test flaky

Jika fuzz target tergantung waktu, random global, atau shared state, hasilnya sulit dipercaya.

20.6 Fuzzing semua hal

Tidak semua function perlu fuzz. Prioritaskan parser, decoder, validator, sanitizer, dan boundary yang menerima input tidak terpercaya.


21. Benchmark dan Fuzzing untuk Function yang Sama

Misal kita punya normalizer:

func NormalizeEmail(s string) string {
    s = strings.TrimSpace(s)
    s = strings.ToLower(s)
    return s
}

Unit test:

func TestNormalizeEmail(t *testing.T) {
    tests := []struct {
        name  string
        input string
        want  string
    }{
        {"trim and lowercase", "  USER@EXAMPLE.COM  ", "user@example.com"},
        {"already normalized", "user@example.com", "user@example.com"},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := NormalizeEmail(tt.input)
            if got != tt.want {
                t.Fatalf("NormalizeEmail(%q) = %q, want %q", tt.input, got, tt.want)
            }
        })
    }
}

Fuzz property:

func FuzzNormalizeEmail(f *testing.F) {
    f.Add("  USER@EXAMPLE.COM  ")
    f.Add("")
    f.Add("こんにちは@EXAMPLE.COM")

    f.Fuzz(func(t *testing.T, input string) {
        got := NormalizeEmail(input)

        if strings.TrimSpace(got) != got {
            t.Fatalf("NormalizeEmail(%q) = %q has surrounding space", input, got)
        }
        if strings.ToLower(got) != got {
            t.Fatalf("NormalizeEmail(%q) = %q is not lowercase", input, got)
        }
    })
}

Benchmark:

var normalizedSink string

func BenchmarkNormalizeEmail(b *testing.B) {
    inputs := []string{
        "user@example.com",
        "  USER@EXAMPLE.COM  ",
        "long.email.address+tag@subdomain.example.com",
    }

    for _, input := range inputs {
        b.Run(input, func(b *testing.B) {
            var out string
            for i := 0; i < b.N; i++ {
                out = NormalizeEmail(input)
            }
            normalizedSink = out
        })
    }
}

Satu function, tiga jenis feedback:

FeedbackPertanyaan
Unit testApakah contoh behavior benar?
Fuzz testApakah property selalu benar untuk input luas?
BenchmarkBerapa cost behavior pada input tertentu?

22. Regression Test dari Bug Production

Setiap bug production seharusnya menjadi test.

Workflow:

Contoh bug:

NormalizeEmail tidak menghapus newline di akhir input.

Regression test:

func TestNormalizeEmailTrimsNewline(t *testing.T) {
    got := NormalizeEmail("USER@EXAMPLE.COM\n")
    want := "user@example.com"

    if got != want {
        t.Fatalf("NormalizeEmail() = %q, want %q", got, want)
    }
}

Kalau bug ditemukan fuzzing, simpan inputnya sebagai seed atau corpus.

Prinsip:

A bug without a regression test is a bug that can return.

23. Performance Regression Guard

Benchmark bisa dipakai sebagai guard informal.

Tidak semua benchmark harus memblokir CI, karena hasil benchmark bisa noisy. Tetapi untuk package performance-critical, kamu bisa:

  • menjalankan benchmark periodik,
  • menyimpan hasil baseline,
  • memakai benchstat di review performa,
  • menjalankan benchmark pada dedicated runner,
  • membuat threshold hanya untuk benchmark stabil.

Contoh script:

go test ./internal/parser -bench=. -benchmem -count=10 > parser-bench.txt

Review PR:

Parser allocation naik dari 1 alloc/op ke 5 alloc/op.
Apakah perubahan ini disengaja?
Apakah path ini hot?
Apakah ada cara mempertahankan readability tanpa allocation tambahan?

Benchmark bukan gate universal. Benchmark adalah alat untuk percakapan berbasis data.


24. Latihan: Parser UserID

Buat function:

type UserID string

func ParseUserID(raw string) (UserID, error) {
    raw = strings.TrimSpace(raw)
    if raw == "" {
        return "", errors.New("user id is required")
    }
    if len(raw) > 64 {
        return "", errors.New("user id is too long")
    }
    for _, r := range raw {
        if !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' || r == '-') {
            return "", errors.New("user id contains invalid character")
        }
    }
    return UserID(raw), nil
}

Unit test:

func TestParseUserID(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    UserID
        wantErr bool
    }{
        {"valid", "user_123", "user_123", false},
        {"trim spaces", " user_123 ", "user_123", false},
        {"empty", "", "", true},
        {"uppercase", "USER", "", true},
        {"slash", "../admin", "", true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseUserID(tt.input)
            if tt.wantErr {
                if err == nil {
                    t.Fatalf("ParseUserID(%q) error = nil, want error", tt.input)
                }
                return
            }
            if err != nil {
                t.Fatalf("ParseUserID(%q) error = %v", tt.input, err)
            }
            if got != tt.want {
                t.Fatalf("ParseUserID(%q) = %q, want %q", tt.input, got, tt.want)
            }
        })
    }
}

Fuzz test:

func FuzzParseUserID(f *testing.F) {
    seeds := []string{
        "user_123",
        "",
        "../admin",
        "USER",
        "hello-world",
        strings.Repeat("a", 65),
    }

    for _, seed := range seeds {
        f.Add(seed)
    }

    f.Fuzz(func(t *testing.T, raw string) {
        id, err := ParseUserID(raw)
        if err != nil {
            return
        }

        if id == "" {
            t.Fatalf("ParseUserID(%q) returned empty id without error", raw)
        }
        if len(id) > 64 {
            t.Fatalf("ParseUserID(%q) returned too long id %q", raw, id)
        }
        for _, r := range string(id) {
            if !(r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' || r == '-') {
                t.Fatalf("ParseUserID(%q) returned invalid id %q", raw, id)
            }
        }
    })
}

Benchmark:

var userIDSink UserID

func BenchmarkParseUserID(b *testing.B) {
    inputs := []string{
        "user_123",
        " user_123 ",
        "hello-world-123",
    }

    for _, input := range inputs {
        b.Run(input, func(b *testing.B) {
            var id UserID
            for i := 0; i < b.N; i++ {
                id, _ = ParseUserID(input)
            }
            userIDSink = id
        })
    }
}

Commands:

go test ./...
go test ./... -run TestParseUserID
go test ./... -fuzz=FuzzParseUserID -fuzztime=30s
go test ./... -bench=BenchmarkParseUserID -benchmem

25. Latihan: Deduplication

Function:

func Deduplicate(xs []string) []string {
    seen := make(map[string]struct{}, len(xs))
    out := make([]string, 0, len(xs))

    for _, x := range xs {
        if _, ok := seen[x]; ok {
            continue
        }
        seen[x] = struct{}{}
        out = append(out, x)
    }

    return out
}

Properties:

  1. output tidak punya duplicate,
  2. semua output berasal dari input,
  3. order first occurrence dipertahankan,
  4. input tidak termutasi.

Table test:

func TestDeduplicate(t *testing.T) {
    tests := []struct {
        name  string
        input []string
        want  []string
    }{
        {"empty", nil, nil},
        {"no duplicate", []string{"a", "b"}, []string{"a", "b"}},
        {"with duplicate", []string{"a", "b", "a"}, []string{"a", "b"}},
        {"preserve first order", []string{"b", "a", "b"}, []string{"b", "a"}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Deduplicate(tt.input)
            if !slices.Equal(got, tt.want) {
                t.Fatalf("Deduplicate(%v) = %v, want %v", tt.input, got, tt.want)
            }
        })
    }
}

Fuzz dengan byte input:

func FuzzDeduplicate(f *testing.F) {
    f.Add("a,b,a")
    f.Add("")
    f.Add("x,x,x")

    f.Fuzz(func(t *testing.T, raw string) {
        input := strings.Split(raw, ",")
        original := append([]string(nil), input...)

        got := Deduplicate(input)

        if !slices.Equal(input, original) {
            t.Fatalf("input mutated: got %v, want %v", input, original)
        }
        if hasDuplicate(got) {
            t.Fatalf("output has duplicate: %v", got)
        }
        for _, x := range got {
            if !contains(original, x) {
                t.Fatalf("output contains value not in input: %q", x)
            }
        }
    })
}

Benchmark size:

func BenchmarkDeduplicate(b *testing.B) {
    for _, size := range []int{10, 100, 1000, 10000} {
        b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
            input := makeDuplicateInput(size)
            b.ResetTimer()

            for i := 0; i < b.N; i++ {
                _ = Deduplicate(input)
            }
        })
    }
}

26. How to Decide: Unit Test, Fuzz, atau Benchmark?

Gunakan matrix ini:

SituasiTool Utama
Behavior domain spesifikUnit/table test
Banyak variasi input-output eksplisitTable test
Public API usage exampleExample test
Parser/validator/sanitizerFuzz test
Edge case tidak mudah dihitung manualFuzz test
Hot path dicurigai lambatBenchmark
Membandingkan dua implementationBenchmark + benchstat
Bug production pernah terjadiRegression test
Race/concurrency riskUnit/concurrency test + -race
Compatibility antar serviceContract/integration test

Jangan memilih tool karena terlihat advanced. Pilih berdasarkan pertanyaan engineering yang ingin dijawab.


27. Checklist Benchmark

Sebelum mempercayai benchmark:

  • Apakah workload realistis?
  • Apakah setup cost sengaja dimasukkan atau sudah dipisah?
  • Apakah hasil computation dipakai agar tidak dieliminasi compiler?
  • Apakah benchmark dijalankan lebih dari sekali?
  • Apakah allocation dilaporkan?
  • Apakah input size bervariasi?
  • Apakah benchmark berjalan pada environment yang cukup stabil?
  • Apakah improvement signifikan secara product/system?
  • Apakah readability dan maintainability tetap baik?
  • Apakah benchmark mengukur path yang memang penting?

28. Checklist Fuzzing

Sebelum menambahkan fuzz test:

  • Apakah target menerima input tidak terpercaya?
  • Apakah function cukup cepat untuk dipanggil berkali-kali?
  • Apakah side effect eksternal dihindari?
  • Apakah seed corpus mencakup edge case penting?
  • Apakah property jelas?
  • Apakah invalid input boleh return error?
  • Apakah panic dianggap bug?
  • Apakah failure bisa direproduksi?
  • Apakah corpus regression disimpan?
  • Apakah fuzzing dijalankan dengan durasi terbatas di workflow lokal/CI?

29. Latihan 20 Jam: Slot Benchmark dan Fuzzing

Dalam 20 jam pertama, alokasikan minimal 2 jam untuk benchmark dan fuzzing.

WaktuFokusOutput
20 menitBenchmark dasar1 benchmark jalan
20 menit-benchmemmembaca ns/op, B/op, allocs/op
20 menitSub-benchmarkbenchmark 3 input size
20 menitFuzz test dasar1 fuzz target dengan seed
20 menitProperty-style thinking3 property tertulis
20 menitRegression workflowmengubah edge case menjadi test

Kriteria lulus:

Kamu bisa membedakan test correctness, benchmark cost, dan fuzzing edge-case discovery; lalu memilih tool yang tepat untuk pertanyaan engineering yang tepat.

30. Ringkasan

Benchmark, fuzzing, dan property-style thinking adalah alat untuk meningkatkan kedalaman engineering.

Hal yang harus diingat:

  • Benchmark mengukur cost, bukan membuktikan correctness.
  • b.N dikontrol oleh test runner.
  • Gunakan -benchmem untuk melihat allocation.
  • Pisahkan setup cost dari measured cost jika perlu.
  • Jangan percaya satu benchmark run untuk klaim serius.
  • Fuzzing mencari input yang tidak kamu bayangkan.
  • Fuzz test paling kuat jika property jelas.
  • Seed corpus adalah starting point, bukan exhaustive list.
  • Fuzz failure harus diperlakukan sebagai regression candidate.
  • Property-style thinking membuat test lebih general daripada contoh manual.
  • Bug production sebaiknya selalu menghasilkan regression test.

Setelah part ini, kamu tidak hanya bisa menulis test Go, tetapi juga mulai bisa memakai toolchain Go untuk mengukur cost dan menemukan edge case secara sistematis.

Part berikutnya akan masuk ke concurrency: goroutine, channel, select, worker pool, pipeline, backpressure, dan goroutine leak.


Referensi Resmi

Lesson Recap

You just completed lesson 12 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.