اختبار الوحدات في لغة جافا سكريبت: الهيكل وال أفضل الممارسات
اختبار 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
يُظهر الإخراج عدد التكرارات في الثانية والخصائص الذاكرة.
أفضل الممارسات
- اكتب اختبارات قائمة على الجداول: استخدم نمط المصفوفة من الهيكل لحالات الاختبار المتعددة
- استخدم
t.Runللاختبارات الفرعية: تنظيم أفضل ويمكن تشغيل الاختبارات الفرعية بشكل انتقائي - ابدأ باختبار الدوال المعلنة أولاً: ركز على سلوك واجهة API العامة
- احتفظ بالاختبارات بسيطة: يجب أن تتحقق كل اختبار من شيء واحد
- استخدم أسماء اختبارات مفيدة: وصف ما يتم اختباره والنتيجة المتوقعة
- لا تختبر التفاصيل التنفيذية: اختبر السلوك، لا التفاصيل الداخلية
- استخدم الواجهات للاعتمادات: يجعل من السهل إنشاء مزيف
- استهدف تغطية عالية، ولكن الجودة أكثر من الكمية: التغطية 100% لا تعني خالية من الأخطاء
- تشغيل الاختبارات مع العلم
-race: اكتشاف مشاكل التزامن مبكرًا - استخدم
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: اختبارات قائمة على الجداول
- مستودع GitHub لـ Testify
- مستندات GoMock
- تعلم Go مع الاختبارات
- أداة تغطية Go
- ورقة ملاحظات Go
- مقارنة مكتبات ORMs في Go لـ PostgreSQL: GORM مقابل Ent مقابل Bun مقابل sqlc
- مكتبات Go لـ Ollama - مقارنة مع أمثلة
- بناء تطبيقات سطر الأوامر في Go مع Cobra & Viper
- النماذج العامة في Go: استخدامات وأنماط
الخاتمة
تُوفر حزمة الاختبارات في Go كل ما يلزم لاختبار الوحدة الشامل مع أقل تجهيز. من خلال اتباع عادات Go مثل اختبارات الجداول، واستخدام الواجهات للاختبارات المزيفة، واستغلال الأدوات المدمجة، يمكنك إنشاء مجموعات اختبارات قابلة للصيانة وموثوقة تنمو مع قاعدة الكود الخاصة بك.
تُطبّق هذه ممارسات الاختبار على جميع أنواع تطبيقات Go، من الخدمات الويب إلى تطبيقات سطر الأوامر المبنية مع Cobra & Viper. يتطلب اختبار أدوات سطر الأوامر أنماطًا مشابهة مع تركيز إضافي على اختبار الإدخال/الإخراج وتحليل الأعلام.
ابدأ باختبارات بسيطة، أضف التغطية تدريجيًا، واتذكر أن الاختبار استثمار في جودة الكود والثقة المكتسبة. يسهل التركيز على الاختبار في مجتمع Go الحفاظ على المشاريع على المدى الطويل والتعاون الفعّال مع أعضاء الفريق.