Go: Go Testing

تأتي المكتبة القياسية للغة Go مزودة بإطار عمل اختبار مدمج — ولا حاجة إلى مكتبات اختبار أو مكتبات تأكيد من جهات خارجية؛ يمكنك كتابة اختبارات عالية الجودة باستخدام حزمة testing الأساسية فقط.

تُعد سلسلة أدوات الاختبار المدمجة في لغة Go فريدة من نوعها في هذا المجال: حيث تقوم go test بالكشف التلقائي عن وظائف الاختبار، ويُعد الاختبار القائم على الجداول سمة مميزة لمجتمع Go، كما تتوفر اختبارات الأداء وتغطية الكود بشكل جاهز للاستخدام فورًا. في هذا الدرس، ستتقن جميع الجوانب الأساسية لاختبار لغة Go.

1. ستتعلم



2. قصة حقيقية لمهندس إعادة هيكلة

(1) المشكلة: أدى تغيير دالة واحدة إلى تعطل ثلاث وحدات في آن واحد

أليس هي مهندسة «باك إند» في فريق المدفوعات. وقد طُلب منها إعادة هيكلة منطق حساب الضرائب في وحدة المدفوعات:

"لقد قمت للتو بتغيير اسم أحد الحقول، معتقدًا أن الأمر سيكون على ما يرام. لكن بعد الإصدار، تبين أن 30% من الطلبات تحتوي على حسابات ضريبية خاطئة — وقال مدير المشروع إننا خسرنا 5,000 دولار. وبما أننا لم نختبر ذلك، لم يعرف أحد أين تم إجراء التغيير."

فتحت شفرة وحدة الدفع واكتشفت أن المشروع بأكمله لا يحتوي على أي ملفات اختبار:

GO
// payment.go — no corresponding payment_test.go
func CalculateTax(amount float64, country string) float64 {
    // No one knows if this function is correct—there are no tests
    switch country {
    case "US":
        return amount * 0.08
    case "CN":
        return amount * 0.13
    default:
        return amount * 0.10
    }
}

بعد إجراء الاختبار، لاحظت أليس المشكلة على الفور — فقد كانت قد حددت سابقًا معدل الضريبة لـ «المملكة المتحدة» بنسبة 20٪ بدلاً من معدل ضريبة القيمة المضافة الصحيح البالغ 20٪ (كان المعدل في الواقع 20٪، ولكن في الحالة الاستثنائية التي تتضمن amount=0، كان الناتج NaN).

(2) Go Solution: إطار عمل الاختبار المدمج

GO
// payment_test.go
package main

import "testing"

// Table-driven test
func TestCalculateTax(t *testing.T) {
    tests := []struct {
        name    string
        amount  float64
        country string
        want    float64
    }{
        {"US standard", 100.0, "US", 8.0},
        {"CN standard", 100.0, "CN", 13.0},
        {"UK default", 100.0, "UK", 10.0},
        {"zero amount", 0.0, "US", 0.0},
        {"negative amount", -50.0, "US", -4.0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := CalculateTax(tt.amount, tt.country)
            if got != tt.want {
                t.Errorf("CalculateTax(%v, %s) = %v, want %v",
                    tt.amount, tt.country, got, tt.want)
            }
        })
    }
}

تشغيل الاختبار:

BASH
$ go test -v
=== RUN   TestCalculateTax/US_standard
=== RUN   TestCalculateTax/CN_standard
=== RUN   TestCalculateTax/UK_default
=== RUN   TestCalculateTax/zero_amount
=== RUN   TestCalculateTax/negative_amount
--- PASS: TestCalculateTax (0.00s)
    --- PASS: TestCalculateTax/US_standard (0.00s)
    --- PASS: TestCalculateTax/CN_standard (0.00s)
    --- PASS: TestCalculateTax/UK_default (0.00s)
    --- PASS: TestCalculateTax/zero_amount (0.00s)
    --- PASS: TestCalculateTax/negative_amount (0.00s)
PASS
ok      payment 0.123s

(3) النتائج: مع إجراء الاختبار مقابل عدم إجرائه

البعد بدون اختبار الاختبار
استعادة الثقة الخوف من تغيير سطر واحد حتى تشغيل go test فور إجراء التغييرات
مشكلات تحديد الموقع تقارير أخطاء المستخدم بعد الإطلاق حالات الفشل في مرحلة التطوير
جودة الكود الحدس الاعتماد على البيانات
البدء الخوف من إجراء التغييرات بمجرد إجراء التغييرات وإجراء الاختبارات، يمكنك أن تطمئن
تكاليف الإرجاع التحقق اليدوي الأتمتة
💡 نصيحة: أفضل الممارسات في مجتمع Go هي الاختبار القائم على الجداول — أي تنظيم حالات الاختبار في جداول (شرائح من الهياكل)، بحيث تحتوي كل حالة على اختبار فرعي واحد. ويُعد هذا النمط أكثر وضوحًا وقابلية للتوسع مقارنةً بالاختبار على مستوى الدوال.



3. testing.T: أساسيات اختبار الوحدات

(1) قواعد الدالة الاختبارية

GO
// Rules:
// 1. File name must end with _test.go
// 2. Function signature must be func TestXxx(t *testing.T)
// 3. Xxx must start with an uppercase letter

// math_test.go
package main

import "testing"

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

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

(2) الطرق الشائعة في الاختبار.T

الطريقة الإجراء هل تواصل؟
t.Log(args...) طباعة سجل (يُعرض فقط عند تحديد الخيار -v)
t.Error(args...) وضع علامة «فشل» + متابعة التنفيذ
t.Errorf(format, args...) خطأ في التنسيق
t.Fatal(args...) وضع علامة «فشل» + إيقاف الاختبار الحالي
t.Fatalf(format, args...) خطأ فادح في التنسيق
t.Skip(args...) تخطي هذا الاختبار

▶ مثال: أربع طرق لكتابة دالة اختبار

GO 📖 للعرض فقط
package main

import (
    "fmt"
    "testing"
)

// Function under test
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, ErrDivisionByZero
    }
    return a / b, nil
}

var ErrDivisionByZero = fmt.Errorf("division by zero")

// Style 1: Simple assertion
func TestDivideBasic(t *testing.T) {
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result != 5.0 {
        t.Errorf("got %f, want %f", result, 5.0)
    }
}

// Style 2: Error vs Fatal
func TestDivideByZero(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Fatal("expected error, got nil")
    }
    if err.Error() != "division by zero" {
        t.Errorf("wrong error message: %v", err)
    }
}

// Style 3: Table-driven test
func TestDivideTable(t *testing.T) {
    tests := []struct {
        name   string
        a, b   float64
        want   float64
        wantErr bool
    }{
        {"10/2", 10, 2, 5, false},
        {"0/5", 0, 5, 0, false},
        {"-6/3", -6, 3, -2, false},
        {"1/0", 1, 0, 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            if tt.wantErr {
                if err == nil {
                    t.Error("expected error")
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if got != tt.want {
                t.Errorf("got %f, want %f", got, tt.want)
            }
        })
    }
}
60 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

4. الاختبار القائم على الجداول (أسلوب Go المميز)

(1) القالب القياسي

GO
package main

import "testing"

func TestMax(t *testing.T) {
    // 1. Define test table
    tests := []struct {
        name string     // Test name (for subtests)
        a, b int        // Input
        want int        // Expected output
    }{
        {name: "first larger", a: 10, b: 3, want: 10},
        {name: "second larger", a: 3, b: 10, want: 10},
        {name: "equal", a: 5, b: 5, want: 5},
        {name: "negative", a: -3, b: -10, want: -3},
        {name: "zero", a: 0, b: 5, want: 5},
    }

    // 2. Iterate through table
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // 3. Execute + assert
            got := Max(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Max(%d, %d) = %d, want %d",
                    tt.a, tt.b, got, tt.want)
            }
        })
    }
}

func Max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

▶ مثال: معالجة قائمة + إدخال معقد

GO 📖 للعرض فقط
package main

import (
    "testing"
)

// Function under test: validate password strength
type PasswordStrength int

const (
    Weak PasswordStrength = iota
    Medium
    Strong
)

func CheckPassword(pwd string) PasswordStrength {
    if len(pwd) < 6 {
        return Weak
    }
    if len(pwd) >= 12 {
        return Strong
    }
    return Medium
}

func TestCheckPassword(t *testing.T) {
    tests := []struct {
        name string
        pwd  string
        want PasswordStrength
    }{
        {"short", "abc", Weak},
        {"medium 6", "abcdef", Medium},
        {"medium 8", "abcdefgh", Medium},
        {"strong 12", "abcdefghijkl", Strong},
        {"empty", "", Weak},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := CheckPassword(tt.pwd)
            if got != tt.want {
                t.Errorf("CheckPassword(%q) = %v, want %v",
                    tt.pwd, got, tt.want)
            }
        })
    }
}
41 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

5. اختبار الأداء testing.B

(1) أساسيات المعايير المرجعية

GO
package main

import (
    "testing"
)

// Benchmark function: func BenchmarkXxx(b *testing.B)
func BenchmarkAdd(b *testing.B) {
    a, c := 100, 200
    for i := 0; i < b.N; i++ {
        Add(a, c)
    }
}
BASH
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: example
BenchmarkAdd-8    1000000000    0.25 ns/op
PASS
ok      example 0.3s

▶ مثال: مقارنة أداء طريقتين لتسلسل السلاسل

GO
// concat_test.go
package main

import (
    "strings"
    "testing"
)

func ConcatPlus(n int) string {
    s := ""
    for i := 0; i < n; i++ {
        s += "a"
    }
    return s
}

func ConcatBuilder(n int) string {
    var sb strings.Builder
    sb.Grow(n)
    for i := 0; i < n; i++ {
        sb.WriteByte('a')
    }
    return sb.String()
}

func BenchmarkConcatPlus(b *testing.B) {
    for i := 0; i < b.N; i++ {
        ConcatPlus(1000)
    }
}

func BenchmarkConcatBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        ConcatBuilder(1000)
    }
}
▶ جرّب الكود
BASH
$ go test -bench=. -benchmem
BenchmarkConcatPlus-8          13134     91238 ns/op   530296 allocs/op
BenchmarkConcatBuilder-8      283321      4221 ns/op       56 allocs/op
💡 نصيحة: إن عملية التسلسل باستخدام + 1,000 مرة أبطأ بـ 20 مرة من استخدام «Builder»، وتستهلك ذاكرة أكبر بـ 10,000 مرة — يتيح لك الخيار -benchmem ملاحظة الفرق في تخصيص الذاكرة.

(3) تفسير نتائج المقارنة المعيارية

عنصر الإخراج المعنى
BenchmarkConcatBuilder-8 اسم الاختبار-8 (8 وحدات معالجة مركزية)
283321 b.N = 283321 تكرارًا
4221 ns/op 4221 نانوثانية لكل عملية
56 allocs/op 56 عملية تخصيص للذاكرة لكل عملية


6. TestMain: نقطة دخول الاختبار

(1) TestMain: الإعداد / الإنهاء

GO
// main_test.go
package main

import (
    "fmt"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    // Setup (runs once for the entire package)
    fmt.Println("=== Setup: Initializing database connection ===")

    // Run all tests
    code := m.Run()

    // Teardown
    fmt.Println("=== Teardown: Closing database connection ===")

    os.Exit(code)
}

func TestSomething(t *testing.T) {
    t.Log("Test A")
}

func TestAnother(t *testing.T) {
    t.Log("Test B")
}
BASH
$ go test -v
=== Setup: Initializing database connection ===
=== RUN   TestSomething
    main_test.go:16: Test A
--- PASS: TestSomething (0.00s)
=== RUN   TestAnother
    main_test.go:20: Test B
--- PASS: TestAnother (0.00s)
=== Teardown: Closing database connection ===
PASS
ok      example 0.1s

▶ مثال: الدالة المساعدة testing.Helper

GO
package main

import "testing"

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

func assertEqual(t testing.TB, got, want interface{}) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

func TestWithHelper(t *testing.T) {
    assertEqual(t, Add(2, 3), 5)
    assertEqual(t, Add(0, 0), 0)
    assertEqual(t, Add(-1, 1), 0)
}
▶ جرّب الكود
💡 نصيحة: يضمن t.Helper() أن يتم تتبع رسائل الخطأ إلى رقم السطر الخاص بالمتصل، وليس داخل الدالة المساعدة نفسها. وتُعد هذه إحدى أفضل الممارسات الأساسية عند كتابة دوال المساعدة الخاصة بالاختبار.



7. التغطية

(1) أساس التغطية

GO
// math.go
package main

func IsEven(n int) bool {
    return n%2 == 0
}

func IsPositive(n int) bool {
    return n > 0
}
GO
// math_test.go
package main

import "testing"

func TestIsEven(t *testing.T) {
    tests := []struct {
        n    int
        want bool
    }{
        {2, true},
        {3, false},
    }
    for _, tt := range tests {
        if got := IsEven(tt.n); got != tt.want {
            t.Errorf("IsEven(%d) = %v", tt.n, got)
        }
    }
}
BASH
$ go test -coverprofile=coverage.out
ok      example 0.1s    coverage: 50.0% of statements

$ go tool cover -html=coverage.out  # Open coverage report in browser

(2) go test -cover: الأوامر الشائعة

الأمر الوظيفة
go test -cover التغطية المعروضة على جهاز العرض
go test -coverprofile=c.out ملف ملف تعريف تغطية الإخراج
go tool cover -html=c.out عرض التقرير المرئي في المتصفح
go test -covermode=count يسجل عدد مرات تنفيذ كل سطر
🔥 خطأ شائع: تغطية الكود بنسبة 100% ≠ كود خالٍ من الأخطاء. تغطية الكود تُخبرك فقط بالأسطر التي تم تنفيذها؛ لكنها لا تُخبرك ما إذا كانت المنطقية صحيحة أم لا. الهدف من كتابة الاختبارات ليس تحقيق رقم معين لتغطية الكود، بل اكتساب الثقة في المسار الحرج.



8. اختبار HTTP باستخدام httptest

(1) httptest.Server + httptest.ResponseRecorder

GO
package main

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

// Handler under test
type UserHandler struct{}

func (h UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    resp := map[string]string{"status": "ok", "message": "hello"}
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

// Test 1: httptest.ResponseRecorder (testing the handler itself)
func TestUserHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/", nil)
    rec := httptest.NewRecorder()

    handler := UserHandler{}
    handler.ServeHTTP(rec, req)

    if rec.Code != http.StatusOK {
        t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)
    }

    var resp map[string]string
    json.Unmarshal(rec.Body.Bytes(), &resp)
    if resp["status"] != "ok" {
        t.Errorf("got status %q, want %q", resp["status"], "ok")
    }
}

// Test 2: httptest.Server (testing a complete HTTP service)
func TestUserHandlerWithServer(t *testing.T) {
    server := httptest.NewServer(UserHandler{})
    defer server.Close()

    resp, err := http.Get(server.URL)
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Errorf("got %d, want %d", resp.StatusCode, http.StatusOK)
    }
}

▶ مثال: httptest + الاختبار القائم على الجداول

GO 📖 للعرض فقط
package main

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

// Handler under test
func greetingHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "World"
    }
    resp := map[string]string{"message": "Hello, " + name + "!"}
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

func TestGreetingHandler(t *testing.T) {
    tests := []struct {
        name   string
        query  string
        want   string
        status int
    }{
        {"with name", "name=Alice", "Hello, Alice!", 200},
        {"empty name", "", "Hello, World!", 200},
        {"with special chars", "name=Go+Lang", "Hello, Go Lang!", 200},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest("GET", "/?"+tt.query, nil)
            rec := httptest.NewRecorder()

            greetingHandler(rec, req)

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

            var resp map[string]string
            json.Unmarshal(rec.Body.Bytes(), &resp)
            if resp["message"] != tt.want {
                t.Errorf("message = %q, want %q", resp["message"], tt.want)
            }
        })
    }
}
43 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)

(3) وضعان لبرنامج httptest

الوضع httptest.NewRecorder httptest.NewServer
موضوع الاختبار معالج واحد خدمة HTTP كاملة
التكاليف العامة للشركة الناشئة لا شيء نعم (تستمع على منفذ عشوائي)
حالات الاستخدام الاختبار الفردي اختبار التكامل
هل يمكن اختبار البرمجيات الوسيطة؟ ✅ تم إنشاؤها يدويًّا ✅ تلقائيًّا عبر مسار العمل بأكمله


9. مثال كامل: إعادة هيكلة مجموعة اختبارات وحدة الدفع

GO
// payment_test.go
package main

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

// ---------- Code under test ----------

type PaymentRequest struct {
    UserID  string  `json:"user_id"`
    Amount  float64 `json:"amount"`
    Country string  `json:"country"`
}

type PaymentResponse struct {
    Success bool   `json:"success"`
    Message string `json:"message,omitempty"`
    Tax     float64 `json:"tax,omitempty"`
    Total   float64 `json:"total,omitempty"`
}

func CalculateTax(amount float64, country string) (float64, error) {
    if amount < 0 {
        return 0, errors.New("negative amount")
    }
    switch country {
    case "US":
        return amount * 0.08, nil
    case "CN":
        return amount * 0.13, nil
    case "DE":
        return amount * 0.19, nil
    default:
        return amount * 0.10, nil
    }
}

func paymentHandler(w http.ResponseWriter, r *http.Request) {
    var req PaymentRequest
    json.NewDecoder(r.Body).Decode(&req)

    tax, err := CalculateTax(req.Amount, req.Country)
    if err != nil {
        json.NewEncoder(w).Encode(PaymentResponse{
            Success: false,
            Message: err.Error(),
        })
        return
    }

    json.NewEncoder(w).Encode(PaymentResponse{
        Success: true,
        Tax:     tax,
        Total:   req.Amount + tax,
    })
}

// ---------- Test code ----------

func jsonBody(s string) *strings.Reader {
    return strings.NewReader(s)
}

// 1. Unit test: CalculateTax table-driven
func TestCalculateTax(t *testing.T) {
    tests := []struct {
        name    string
        amount  float64
        country string
        want    float64
        wantErr bool
    }{
        {"US $100", 100, "US", 8.0, false},
        {"CN $100", 100, "CN", 13.0, false},
        {"DE $100", 100, "DE", 19.0, false},
        {"UK default", 100, "UK", 10.0, false},
        {"zero amount", 0, "US", 0, false},
        {"negative amount", -100, "US", 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := CalculateTax(tt.amount, tt.country)
            if tt.wantErr {
                if err == nil {
                    t.Error("expected error")
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if got != tt.want {
                t.Errorf("CalculateTax(%v, %s) = %v, want %v",
                    tt.amount, tt.country, got, tt.want)
            }
        })
    }
}

// 2. Benchmark: different ways to calculate tax
func BenchmarkCalculateTax(b *testing.B) {
    for i := 0; i < b.N; i++ {
        CalculateTax(100.0, "US")
    }
}

// 3. HTTP test: paymentHandler
func TestPaymentHandler(t *testing.T) {
    tests := []struct {
        name        string
        body        string
        wantStatus  int
        wantSuccess bool
    }{
        {"US payment", `{"user_id":"u1","amount":100,"country":"US"}`, 200, true},
        {"CN payment", `{"user_id":"u2","amount":200,"country":"CN"}`, 200, true},
        {"negative amount", `{"user_id":"u3","amount":-50,"country":"US"}`, 200, false},
        {"invalid JSON", `not json`, 200, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest("POST", "/pay",
                jsonBody(tt.body))
            req.Header.Set("Content-Type", "application/json")
            rec := httptest.NewRecorder()

            paymentHandler(rec, req)

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

            var resp PaymentResponse
            json.Unmarshal(rec.Body.Bytes(), &resp)
            if resp.Success != tt.wantSuccess {
                t.Errorf("success = %v, want %v", resp.Success, tt.wantSuccess)
            }
        })
    }
}
100%
sequenceDiagram
    participant Dev
    participant Terminal as go test
    participant Package as Package Under Test
    participant Coverage as coverage.out

    Dev->>Terminal: go test -v -cover
    Terminal->>Package: 1. Find *_test.go
    Terminal->>Package: 2. Call TestMain(m)
    Package->>Package: 3. Setup
    Package->>Package: 4. Execute all TestXxx
    Package->>Package: 5. Execute BenchmarkXxx
    Package->>Package: 6. Teardown
    Package-->>Terminal: PASS / FAIL
    Terminal-->>Coverage: Coverage data
    Terminal-->>Dev: Result summary
🔥 خطأ شائع: يجب أن تحمل ملفات الاختبار الامتداد _test.go؛ وإلا فلن تعمل go test. ويجب أن تكون توقيعات الدالة بالضبط func TestXxx(t *testing.T) — حيث تكون المعلمة *testing.T، وليست *testing.TT أو testing.T.



❓ أسئلة شائعة

س كيف أكتب اختبارات تعتمد على الجداول؟
ج حدد []struct كجدول الاختبار، بحيث تحتوي كل حالة على name + المدخلات + الناتج المتوقع. قم بالتكرار عبر الجدول واستخدم t.Run(tt.name, ...) لتنفيذ الاختبارات الفرعية. يعتبر مجتمع Go أن هذه هي الطريقة القياسية لكتابة الاختبارات.
س كيف يمكنني تشغيل اختبار أداء؟
ج go test -bench=. يُشغّل جميع اختبارات الأداء؛ go test -bench=FuncName يُشغّل وظيفة محددة؛ -benchmem يعرض معلومات تخصيص الذاكرة. يتم تحديد b.N تلقائيًا بواسطة الإطار.
س كيف يمكنني التحقق من التغطية؟
ج تعرض go test -cover النسبة المئوية؛ وتقوم go test -coverprofile=c.out بإخراج ملف؛ بينما توفر go tool cover -html=c.out عرضًا مرئيًا عبر المتصفح. المعيار هو 70%+، ويُوصى بنسبة 90%+ للمنطق الأساسي.
س كيف يمكنني استخدام httptest لاختبار معالج HTTP؟
ج هناك طريقتان: httptest.NewRecorder() لاختبار المعالج مباشرةً (اختبار الوحدة)، وhttptest.NewServer(handler) لتشغيل خادم HTTP حقيقي لأغراض الاختبار (اختبار التكامل). نوصي باستخدام أداة التسجيل (Recorder) أولاً.
س ما هو ترتيب إجراءات الإعداد (Setup) والتفكيك (Teardown) في TestMain؟
ج يتم تنفيذ TestMain مرة واحدة قبل جميع دوال الاختبار في الحزمة. يتم تنفيذ الإعداد قبل m.Run()، ويتم تنفيذ التفكيك بعده. ملاحظة: يتم تخزين المخرجات مؤقتًا، لذا يجب عليك استخدام os.Exit(code) للخروج.
س ما الذي يخزنه go test في ذاكرة التخزين المؤقت؟
ج بشكل افتراضي، يقوم go test بتخزين نتائج الاختبار (بناءً على الكود والبيئة) في ذاكرة التخزين المؤقت. وإذا لم تحدث أي تغييرات عند إعادة تشغيله، فسيعرض (cached). استخدم go test -count=1 لإجباره على إعادة التشغيل.
س كيف يمكنني تخطي اختبارات معينة؟
ج t.Skip("reason") يتخطى الاختبار الحالي؛ testing.Short() يعمل مع go test -short لتخطي الاختبارات التي تستغرق وقتًا طويلاً؛ t.Skipf(format, args...) لتخطي الاختبارات بتنسيق معين.

📖 ملخص


📝 تمارين

  1. المسألة الأساسية (الصعوبة ⭐): اكتب اختبارًا قائمًا على الجداول لـ max(nums ...int) int من الدرس 4 (الدوال)، بحيث يغطي الحالات الخمس التالية: الأعداد الموجبة، والأعداد السالبة، ومزيج من الأعداد الموجبة والسالبة، وعنصر واحد، والوسائط الفارغة.

  2. تمرين متقدم (درجة الصعوبة ⭐⭐): اكتب اختبارات لـ Exporter من الدرس 10 (إدخال/إخراج الملفات و JSON): استخدم os.CreateTemp لإنشاء ملف مؤقت لاختبار عملية التصدير؛ استخدم json.Unmarshal للتحقق من صحة المحتوى؛ استخدم t.Cleanup لمسح الملف المؤقت.

  3. التحدي (الصعوبة: ⭐⭐⭐): اكتب مجموعة اختبارات كاملة لنظام بوابة الدفع المتعددة من الدرس 7: (1) الاختبار القائم على الجداول لـ MockPaymentGateway (يعطي نتائج ثابتة)؛ (2) قم بإجراء مقارنة أداء لسرعة تطبيقات Stripe و PayPal؛ (3) استخدم httptest لاختبار تنسيق استجابة JSON لمعالج الدفع؛ (4) تغطية الكود ≥ 90%.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%