Go با مثال: تست و بنچمارک

تست واحد (unit testing) بخش مهمی از نوشتن برنامه‌های اصولی Go است. بستهٔ testing ابزارهای لازم برای نوشتن تست واحد را فراهم می‌کند و دستور go test تست‌ها را اجرا می‌کند.

برای نمایش، این کد در بستهٔ main است، اما می‌تواند در هر بسته‌ای باشد. کد تست معمولاً در همان بسته‌ای قرار می‌گیرد که کدِ مورد تست در آن است.

package main
import (
    "fmt"
    "testing"
)

این پیاده‌سازی ساده‌ی کمینهٔ دو عدد صحیح را تست می‌کنیم. معمولاً کدی که تست می‌کنیم در فایلی با نامی مثل intutils.go قرار دارد و فایل تست آن intutils_test.go نامیده می‌شود.

func IntMin(a, b int) int {
    if a < b {
        return a
    }
    return b
}

یک تست با نوشتن تابعی ساخته می‌شود که نامش با Test شروع شود.

func TestIntMinBasic(t *testing.T) {
    ans := IntMin(2, -2)
    if ans != -2 {

t.Error* شکست تست را گزارش می‌کند اما اجرای تست را ادامه می‌دهد. t.Fatal* شکست را گزارش می‌کند و تست را بلافاصله متوقف می‌کند.

        t.Errorf("IntMin(2, -2) = %d; want -2", ans)
    }
}

نوشتن تست‌ها می‌تواند تکراری باشد، پس روش رایج استفاده از سبک جدول‌محور (table-driven) است؛ ورودی‌های تست و خروجی‌های مورد انتظار در یک جدول فهرست می‌شوند و یک حلقهٔ واحد روی آن‌ها پیمایش می‌کند و منطق تست را اجرا می‌کند.

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

t.Run امکان اجرای «زیرتست‌ها» را می‌دهد؛ یکی برای هر سطر جدول. این‌ها هنگام اجرای go test -v جداگانه نمایش داده می‌شوند.

    for _, tt := range tests {
        testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
        t.Run(testname, func(t *testing.T) {
            ans := IntMin(tt.a, tt.b)
            if ans != tt.want {
                t.Errorf("got %d, want %d", ans, tt.want)
            }
        })
    }
}

تست‌های بنچمارک معمولاً در فایل‌های _test.go قرار می‌گیرند و نامشان با Benchmark شروع می‌شود. هر کدی که برای اجرای بنچمارک لازم است اما نباید اندازه‌گیری شود، پیش از این حلقه قرار می‌گیرد.

func BenchmarkIntMin(b *testing.B) {
    for b.Loop() {

اجراکنندهٔ بنچمارک این بدنهٔ حلقه را خودکار بارها اجرا می‌کند تا تخمین معقولی از زمان اجرای یک تکرار به دست آورد.

        IntMin(1, 2)
    }
}

همهٔ تست‌های پروژهٔ فعلی را در حالت مفصل (verbose) اجرا کنید.

$ go test -v
== RUN   TestIntMinBasic
--- PASS: TestIntMinBasic (0.00s)
=== RUN   TestIntMinTableDriven
=== RUN   TestIntMinTableDriven/0,1
=== RUN   TestIntMinTableDriven/1,0
=== RUN   TestIntMinTableDriven/2,-2
=== RUN   TestIntMinTableDriven/0,-1
=== RUN   TestIntMinTableDriven/-1,0
--- PASS: TestIntMinTableDriven (0.00s)
    --- PASS: TestIntMinTableDriven/0,1 (0.00s)
    --- PASS: TestIntMinTableDriven/1,0 (0.00s)
    --- PASS: TestIntMinTableDriven/2,-2 (0.00s)
    --- PASS: TestIntMinTableDriven/0,-1 (0.00s)
    --- PASS: TestIntMinTableDriven/-1,0 (0.00s)
PASS
ok      examples/testing-and-benchmarking    0.023s

همهٔ بنچمارک‌های پروژهٔ فعلی را اجرا کنید. همهٔ تست‌ها پیش از بنچمارک‌ها اجرا می‌شوند. فلگ bench نام توابع بنچمارک را با یک regexp فیلتر می‌کند.

$ go test -bench=.
goos: darwin
goarch: arm64
pkg: examples/testing
BenchmarkIntMin-8 1000000000 0.3136 ns/op
PASS
ok      examples/testing-and-benchmarking    0.351s

مثال بعدی: .