مقدمة حول اختبار الوحدات

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

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

القيام بالاختبارات الأساسية باستخدام حزمة testing

تتضمن مكتبة اللغة Go القياسية حزمة testing التي توفر الأدوات والوظائف لكتابة الاختبارات وتشغيلها.

2.1 إنشاء أول حالة اختبار

لكتابة دالة اختبار، تحتاج إلى إنشاء ملف بلاحقة _test.go. على سبيل المثال، إذا كان اسم ملف الكود المصدري هو calculator.go، يجب أن يكون اسم ملف الاختبار calculator_test.go.

بعد ذلك، حان الوقت لإنشاء دالة الاختبار. تحتاج دالة الاختبار إلى استيراد حزمة testing واتباع تنسيق معين. إليك مثال بسيط:

// calculator_test.go
package calculator

import (
	"testing"
	"fmt"
)

// اختبار دالة الجمع
func TestAdd(t *testing.T) {
	result := Add(1, 2)
	expected := 3

if result != expected {
		t.Errorf("توقعت %v، ولكن حصلت على %v", expected, result)
	}
}

في هذا المثال، TestAdd هي دالة اختبار تختبر دالة Add وهمية. إذا تطابقت نتيجة دالة Add مع النتيجة المتوقعة، سينجح الاختبار؛ وإلا، سيتم استدعاء t.Errorf لتسجيل المعلومات حول فشل الاختبار.

2.2 فهم قواعد التسمية وتوقيع دوال الاختبار

يجب أن تبدأ دوال الاختبار بـ Test، تليها سلسلة غير صغيرة، ويجب أن يكون معاملها الوحيد هو مؤشر على testing.T. كما هو موضح في المثال، تتبع TestAdd التسمية والتوقيع الصحيحة.

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

يمكنك تشغيل حالات الاختبار الخاصة بك باستخدام أداة سطر الأوامر. لحالة اختبار معينة، اشغل الأمر التالي:

go test -v // تشغيل الاختبارات في الدليل الحالي وعرض نتائج مفصلة

إذا كنت ترغب في تشغيل حالة اختبار معينة، يمكنك استخدام العلامة -run تليها تعبير منتظم:

go test -v -run TestAdd // تشغيل فقط دالة الاختبار TestAdd

سيعثر أمر go test تلقائياً على جميع ملفات _test.go ويشغل كل دالة اختبار تلبي المعايير. إذا نجحت جميع الاختبارات، سترى رسالة مماثلة ل PASS في سطر الأوامر؛ وإذا فشل أي اختبار، سترى FAIL إلى جانب رسالة الخطأ المقابلة.

3. كتابة حالات الاختبار

3.1 الإبلاغ عن الأخطاء باستخدام t.Errorf و t.Fatalf

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

مثال على استخدام Errorf:

func TestAdd(t *testing.T) {
    got := Add(1, 2)
    want := 3
    if got != want {
        t.Errorf("Add(1, 2) = %d; توقعت %d", got, want)
    }
}

إذا كنت ترغب في إيقاف الاختبار على الفور عند اكتشاف خطأ، يمكنك استخدام Fatalf:

func TestSubtract(t *testing.T) {
    got := Subtract(5, 3)
    if got != 2 {
        t.Fatalf("Subtract(5, 3) = %d; توقعت 2", got)
    }
}

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

3.2 تنظيم الاختبارات الفرعية وتشغيل الاختبارات الفرعية

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

مثال على استخدام الاختبار الفرعي t.Run:

func TestMultiply(t *testing.T) {
    testcases := []struct {
        name           string
        a, b, expected int
    }{
        {"2x3", 2, 3, 6},
        {"-1x-1", -1, -1, 1},
        {"0x4", 0, 4, 0},
    }

    for _, tc := range testcases {
        t.Run(tc.name, func(t *testing.T) {
            if got := Multiply(tc.a, tc.b); got != tc.expected {
                t.Errorf("ضرب(%d, %d) = %d; يريد %d", tc.a, tc.b, got, tc.expected)
            }
        })
    }
}

إذا أردنا تشغيل الاختبار الفرعي بعنوان "2x3" بشكل فردي، يمكننا تشغيل الأمر التالي في سطر الأوامر:

go test -run TestMultiply/2x3

يرجى ملاحظة أن أسماء الاختبارات الفرعية حساسة لحالة الأحرف.

4. العمل التحضيري قبل وبعد الاختبار

4.1 الإعداد وتفكيك الإعداد

عند إجراء الاختبارات، غالبًا ما نحتاج إلى إعداد حالة بداية للاختبارات (مثل اتصال قاعدة البيانات، إنشاء ملف، إلخ)، وبالمثل، نحتاج إلى القيام ببعض الأعمال التنظيف بعد الانتهاء من الاختبارات. في Go، نقوم عادةً بأداء Setup و Teardown مباشرةً في دوال الاختبار، ويوفر لنا الدالة t.Cleanup القدرة على تسجيل وظائف الارتداد للتنظيف.

إليك مثال بسيط:

func TestDatabase(t *testing.T) {
    db, err := SetupDatabase()
    if err != nil {
        t.Fatalf("فشل الإعداد: %v", err)
    }

    // سجل وظيفة ارتداد لضمان إغلاق اتصال قاعدة البيانات عند انتهاء الاختبار
    t.Cleanup(func() {
        if err := db.Close(); err != nil {
            t.Errorf("فشل في إغلاق قاعدة البيانات: %v", err)
        }
    })

    // قم بالاختبار...
}

في دالة TestDatabase، نستدعي أولاً دالة SetupDatabase لإعداد بيئة الاختبار. ثم نستخدم t.Cleanup() لتسجيل دالة ستتم استدعاؤها بعد اكتمال الاختبار للقيام بأعمال تنظيف. في هذا المثال، سيتم إغلاق اتصال قاعدة البيانات. بهذه الطريقة، يمكننا التأكد من تحرير الموارد بشكل صحيح بغض النظر عما إذا نجح الاختبار أم فشل.

5. تحسين كفاءة الاختبار

تحسين كفاءة الاختبار يمكن أن يساعدنا في التطوير بشكل أسرع واكتشاف المشكلات بسرعة وضمان جودة الكود. فيما يلي، سنناقش تغطية الاختبار، اختبارات قائمة على الجدول، واستخدام المحاكاة لتحسين كفاءة الاختبار.

5.1 تغطية الاختبار والأدوات ذات الصلة

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

باستخدام الأمر go test -cover، يمكنك رؤية نسبة تغطية الاختبار الحالية:

go test -cover

إذا كنت ترغب في فهم أكثر تفصيلي حول السطور التي تم تنفيذها والتي لم يتم تنفيذها، يمكنك استخدام المعلمة -coverprofile التي تُنشئ ملف بيانات التغطية. ثم يمكنك استخدام الأمر go tool cover لإنشاء تقرير تغطية الاختبار بشكل مفصل.

go test -coverprofile=coverage.out
go tool cover -html=coverage.out

سيفتح الأمر أعلاه تقريرًا ويبيًا يُظهر بصريًا السطور التي تم تغطيتها بواسطة الاختبارات وتلك التي لم تتم تغطيتها. اللون الأخضر يُمثل الكود الذي تم تغطيته بواسطة الاختبارات، في حين يُمثل اللون الأحمر الأسطر التي لم يتم تغطيتها.

5.2 استخدام الـ Mocks

في عمليات الاختبار، نواجه غالباً حالات نحتاج فيها لمحاكاة الاعتمادات الخارجية. يمكن أن تساعدنا الـ Mocks في محاكاة هذه الاعتمادات، مما يتيح لنا التخلص من الحاجة للتوجه بشكل محدد للخدمات أو الموارد الخارجية في بيئة الاختبار.

هناك العديد من أدوات الـ Mocks في مجتمع جو، مثل testify/mock و gomock. توفر هذه الأدوات عادة مجموعة من واجهات برمجة التطبيقات (APIs) لإنشاء واستخدام كائنات الـ Mock.

فيما يلي مثال أساسي عن استخدام testify/mock. أول شيء يجب فعله هو تحديد واجهة ونسختها الوهمية:

type DataService interface {
    FetchData() (int, error)
}

type MockDataService struct {
    mock.Mock
}

func (m *MockDataService) FetchData() (int, error) {
    args := m.Called()
    return args.Int(0), args.Error(1)
}

في الاختبار، يمكننا استخدام MockDataService لاستبدال خدمة البيانات الفعلية:

func TestSomething(t *testing.T) {
    mockDataSvc := new(MockDataService)
    mockDataSvc.On("FetchData").Return(42, nil) // تكوين السلوك المتوقع

    result, err := mockDataSvc.FetchData() // استخدام الكائن الوهمي
    assert.NoError(t, err)
    assert.Equal(t, 42, result)

    mockDataSvc.AssertExpectations(t) // التحقق مما إذا حدث السلوك المتوقع
}

من خلال الطريقة المذكورة أعلاه، يمكننا تجنب الاعتماد على الخدمات الخارجية أو استقطابها، مثل استدعاءات قاعدة البيانات وما إلى ذلك خلال الاختبار. يمكن أن يسرع هذا من تنفيذ الاختبار ويجعل اختباراتنا أكثر استقرارًا وموثوقية.

6. تقنيات الاختبار المتقدمة

بعد اتقان أساسيات اختبارات الوحدات في جو، يمكننا أن نستكشف بشكل أعمق بعض تقنيات الاختبار المتقدمة، التي تساعد في بناء برمجيات أكثر صلابة وتحسين كفاءة الاختبار.

6.1 اختبار الوظائف الخاصة

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

اختبار الوظائف الخاصة يختلف عن اختبار الوظائف العامة لأنه لا يمكن الوصول إليها من خارج الحزمة. تقنية شائعة هي كتابة كود الاختبار في نفس الحزمة مما يسمح بالوصول إلى الوظيفة الخاصة.

فيما يلي مثال بسيط:

// calculator.go
package calculator

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

الملف المقابل للاختبار كالتالي:

// calculator_test.go
package calculator

import "testing"

func TestAdd(t *testing.T) {
    expected := 4
    actual := add(2, 2)
    if actual != expected {
        t.Errorf("متوقع %d، تم الحصول على %d", expected, actual)
    }
}

من خلال وضع ملف الاختبار في نفس الحزمة، يمكننا اختبار الوظيفة add مباشرة.

6.2 أنماط الاختبار الشائعة وأفضل الممارسات

يحتوي اختبار الوحدات في جو على بعض الأنماط الشائعة التي تيسر عملية الاختبار وتساعد في الحفاظ على وضوح الكود وصيانته.

  1. اختبارات المدخلات والمخرجات بشكل جدولي

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

    // calculator_test.go
    package calculator

    import "testing"

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

        for _, tt := range tests {
            testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
            t.Run(testname, func(t *testing.T) {
                ans := add(tt.a, tt.b)
                if tt.want != ans {
                    t.Errorf("تم الحصول على %d، متوقع %d", ans, tt.want)
                }
            })
        }
    }
  1. استخدام الـ Mocks في الاختبار

    الـ Mocking هو تقنية اختبار تشمل استبدال الاعتمادات لاختبار أجزاء مختلفة من الوظائف. في جو، الواجهات هي الطريقة الرئيسية لتنفيذ الـ Mocks. من خلال استخدام الواجهات، يمكن إنشاء تنفيذ وهمي واستخدامه في الاختبار.