اختبار الوحدات في لغة جافا سكريبت: الهيكل وال أفضل الممارسات

اختبار Go من الأساسيات إلى الأنماط المتقدمة

Page content

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

اختبارات Go الوحدوية رائعة

لماذا يهم الاختبار في Go

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

الفوائد الرئيسية لاختبار Go:

  • الدعم المدمج: لا حاجة لاطراف خارجية
  • التنفيذ السريع: تنفيذ الاختبارات بالتوازي بشكل افتراضي
  • اللغة البسيطة: كود مسحوب قليل
  • أدوات غنية: تقارير التغطية، والاختبارات، والتحليل
  • ودي مع CI/CD: سهولة التكامل مع أنظمة التصنيع التلقائية

هيكل المشروع لاختبارات Go

توجد اختبارات Go بجانب كود الإنتاج مع تسمية واضحة:

myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│   ├── helper.go
│   └── helper_test.go
└── models/
    ├── user.go
    └── user_test.go

القواعد الأساسية:

  • تنتهي ملفات الاختبار بـ _test.go
  • توجد الاختبارات في نفس الحزمة (أو تستخدم للاحتمالات _test للاختبارات المغلقة)
  • يمكن لكل ملف مصدر أن يكون له ملف اختبار مطابق

طرق اختبار الحزمة

الاختبارات المفتوحة (نفس الحزمة):

package calculator

import "testing"
// يمكن الوصول إلى الدوال والمتغيرات غير المعلنة

الاختبارات المغلقة (حزمة خارجية):

package calculator_test

import (
    "testing"
    "myproject/calculator"
)
// يمكن الوصول فقط إلى الدوال المعلنة (الموصى بها لواجهات API العامة)

هيكل الاختبار الأساسي

يتم اتباع هذا النمط لكل دالة اختبار:

package calculator

import "testing"

// يجب أن تبدأ دالة الاختبار بـ "Test"
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

طرق testing.T:

  • t.Error() / t.Errorf(): علامة فشل الاختبار ولكن الاستمرار
  • t.Fatal() / t.Fatalf(): علامة فشل الاختبار ووقف الفورية
  • t.Log() / t.Logf(): تسجيل الناتج (يظهر فقط مع العلم -v)
  • t.Skip() / t.Skipf(): تخطي الاختبار
  • t.Parallel(): تشغيل الاختبار بالتوازي مع اختبارات أخرى

الاختبارات القائمة على الجداول: الطريقة المعتادة في Go

تُعتبر الاختبارات القائمة على الجداول الطريقة المعتادة في Go لاختبار حالات متعددة. مع النماذج العامة في Go، يمكنك أيضًا إنشاء مساعدين للاختبار آمنين من النمط الذي يعمل عبر أنواع بيانات مختلفة:

func TestCalculate(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        op       string
        expected int
        wantErr  bool
    }{
        {"addition", 2, 3, "+", 5, false},
        {"subtraction", 5, 3, "-", 2, false},
        {"multiplication", 4, 3, "*", 12, false},
        {"division", 10, 2, "/", 5, false},
        {"division by zero", 10, 0, "/", 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Calculate(tt.a, tt.b, tt.op)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            
            if result != tt.expected {
                t.Errorf("Calculate(%d, %d, %q) = %d; want %d", 
                    tt.a, tt.b, tt.op, result, tt.expected)
            }
        })
    }
}

المزايا:

  • دالة اختبار واحدة لعدة حالات
  • سهولة إضافة حالات اختبار جديدة
  • وثائق واضحة عن السلوك المتوقع
  • تنظيم أفضل واختبارات أكثر قابلية للصيانة

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

الأوامر الأساسية

# تشغيل الاختبارات في الدليل الحالي
go test

# تشغيل الاختبارات مع إخراج مفصل
go test -v

# تشغيل الاختبارات في جميع الدلائل الفرعية
go test ./...

# تشغيل اختبار معين
go test -run TestAdd

# تشغيل اختبارات مطابقة للنمط
go test -run TestCalculate/addition

# تشغيل الاختبارات بالتوازي (الافتراضي هو GOMAXPROCS)
go test -parallel 4

# تشغيل الاختبارات مع مهلة
go test -timeout 30s

تغطية الاختبار

# تشغيل الاختبارات مع تغطية
go test -cover

# إنشاء ملف تغطية
go test -coverprofile=coverage.out

# عرض التغطية في المتصفح
go tool cover -html=coverage.out

# عرض التغطية حسب الدالة
go tool cover -func=coverage.out

# تحديد وضع التغطية (set، count، atomic)
go test -covermode=count -coverprofile=coverage.out

الأعلام المفيدة

  • -short: تشغيل الاختبارات التي تحمل if testing.Short()
  • -race: تمكين مكتشف السباق (يكتشف مشاكل الوصول المتزامن)
  • -cpu: تحديد قيم GOMAXPROCS
  • -count n: تشغيل كل اختبار n مرة
  • -failfast: التوقف عند أول فشل

مساعدين للاختبار والتهيئة/الإنهاء

الدوال المساعدة

قم بتسمية الدوال المساعدة بـ t.Helper() لتحسين تقارير الأخطاء:

func assertEqual(t *testing.T, got, want int) {
    t.Helper() // هذه السطر يُعتبر المُعلِّن
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestMath(t *testing.T) {
    result := Add(2, 3)
    assertEqual(t, result, 5) // خط الأخطاء يشير هنا
}

التهيئة والإنهاء

func TestMain(m *testing.M) {
    // الكود الخاص بالتهيئة
    setup()
    
    // تشغيل الاختبارات
    code := m.Run()
    
    // الكود الخاص بالإنهاء
    teardown()
    
    os.Exit(code)
}

مجموعات الاختبار

func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("setup test case")
    return func(t *testing.T) {
        t.Log("teardown test case")
    }
}

func TestSomething(t *testing.T) {
    teardown := setupTestCase(t)
    defer teardown(t)
    
    // كود الاختبار هنا
}

التزامن والحقول المُستوردة

التزامن القائم على الواجهات

عند اختبار الكود الذي يتفاعل مع قواعد البيانات، يجعل استخدام الواجهات من السهل إنشاء تنفيذات مزيفة. إذا كنت تعمل مع PostgreSQL في Go، راجع مقارنتنا بين مكتبات ORMs في Go لاختيار المكتبة المناسبة مع قابلية اختبار جيدة.

// الكود الإنتاجي
type Database interface {
    GetUser(id int) (*User, error)
}

type UserService struct {
    db Database
}

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

// كود الاختبار
type MockDatabase struct {
    users map[int]*User
}

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

func TestGetUserName(t *testing.T) {
    mockDB := &MockDatabase{
        users: map[int]*User{
            1: {ID: 1, Name: "Alice"},
        },
    }
    
    service := &UserService{db: mockDB}
    name, err := service.GetUserName(1)
    
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if name != "Alice" {
        t.Errorf("got %s, want Alice", name)
    }
}

المكتبات الشائعة للاختبار

Testify

أفضل مكتبة اختبار في Go للاختبارات والمساعدين:

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

func TestWithTestify(t *testing.T) {
    result := Add(2, 3)
    assert.Equal(t, 5, result, "they should be equal")
    assert.NotNil(t, result)
}

// مثال على المساعد
type MockDB struct {
    mock.Mock
}

func (m *MockDB) GetUser(id int) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}

أدوات أخرى

  • gomock: إطار عمل مزيف من Google مع إنشاء الكود
  • httptest: مكتبة معيارية لاختبار مُعالجي HTTP
  • testcontainers-go: اختبارات التكامل مع حاويات Docker
  • ginkgo/gomega: إطار عمل اختبارات من نوع BDD

عند اختبار التكامل مع خدمات خارجية مثل نماذج الذكاء الاصطناعي، ستحتاج إلى مزيف أو تثبيت تلك الاعتماديات. على سبيل المثال، إذا كنت تستخدم Ollama في Go، ففكر في إنشاء مُغلفات واجهات لجعل كودك أكثر قابلية للاختبار.

اختبارات الأداء

تتضمن Go دعمًا مدمجًا لاختبارات الأداء:

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

// تشغيل اختبارات الأداء
// go test -bench=. -benchmem

يُظهر الإخراج عدد التكرارات في الثانية والخصائص الذاكرة.

أفضل الممارسات

  1. اكتب اختبارات قائمة على الجداول: استخدم نمط المصفوفة من الهيكل لحالات الاختبار المتعددة
  2. استخدم t.Run للاختبارات الفرعية: تنظيم أفضل ويمكن تشغيل الاختبارات الفرعية بشكل انتقائي
  3. ابدأ باختبار الدوال المعلنة أولاً: ركز على سلوك واجهة API العامة
  4. احتفظ بالاختبارات بسيطة: يجب أن تتحقق كل اختبار من شيء واحد
  5. استخدم أسماء اختبارات مفيدة: وصف ما يتم اختباره والنتيجة المتوقعة
  6. لا تختبر التفاصيل التنفيذية: اختبر السلوك، لا التفاصيل الداخلية
  7. استخدم الواجهات للاعتمادات: يجعل من السهل إنشاء مزيف
  8. استهدف تغطية عالية، ولكن الجودة أكثر من الكمية: التغطية 100% لا تعني خالية من الأخطاء
  9. تشغيل الاختبارات مع العلم -race: اكتشاف مشاكل التزامن مبكرًا
  10. استخدم TestMain للتهيئة المكلفة: تجنب تكرار التهيئة في كل اختبار

مثال: مجموعة اختبارات كاملة

package user

import (
    "errors"
    "testing"
)

type User struct {
    ID    int
    Name  string
    Email string
}

func ValidateUser(u *User) error {
    if u.Name == "" {
        return errors.New("name cannot be empty")
    }
    if u.Email == "" {
        return errors.New("email cannot be empty")
    }
    return nil
}

// ملف الاختبار: user_test.go
func TestValidateUser(t *testing.T) {
    tests := []struct {
        name    string
        user    *User
        wantErr bool
        errMsg  string
    }{
        {
            name:    "valid user",
            user:    &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
            wantErr: false,
        },
        {
            name:    "empty name",
            user:    &User{ID: 1, Name: "", Email: "alice@example.com"},
            wantErr: true,
            errMsg:  "name cannot be empty",
        },
        {
            name:    "empty email",
            user:    &User{ID: 1, Name: "Alice", Email: ""},
            wantErr: true,
            errMsg:  "email cannot be empty",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateUser(tt.user)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            
            if err != nil && err.Error() != tt.errMsg {
                t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
            }
        })
    }
}

روابط مفيدة

الخاتمة

تُوفر حزمة الاختبارات في Go كل ما يلزم لاختبار الوحدة الشامل مع أقل تجهيز. من خلال اتباع عادات Go مثل اختبارات الجداول، واستخدام الواجهات للاختبارات المزيفة، واستغلال الأدوات المدمجة، يمكنك إنشاء مجموعات اختبارات قابلة للصيانة وموثوقة تنمو مع قاعدة الكود الخاصة بك.

تُطبّق هذه ممارسات الاختبار على جميع أنواع تطبيقات Go، من الخدمات الويب إلى تطبيقات سطر الأوامر المبنية مع Cobra & Viper. يتطلب اختبار أدوات سطر الأوامر أنماطًا مشابهة مع تركيز إضافي على اختبار الإدخال/الإخراج وتحليل الأعلام.

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