مقدمة حول اختبار الوحدات
اختبار الوحدات يشير إلى التحقق والتحقق من أصغر وحدة يمكن اختبارها في برنامج، مثل الدالة أو الطريقة في لغة 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 أنماط الاختبار الشائعة وأفضل الممارسات
يحتوي اختبار الوحدات في جو على بعض الأنماط الشائعة التي تيسر عملية الاختبار وتساعد في الحفاظ على وضوح الكود وصيانته.
-
اختبارات المدخلات والمخرجات بشكل جدولي
اختبارات المدخلات والمخرجات بشكل جدولي هي طريقة لتنظيم مدخلات الاختبار والمخرجات المتوقعة. من خلال تحديد مجموعة من حالات الاختبار، ثم الحلقة عبرها للقيام بالاختبار، تجعل هذه الطريقة من السهل جداً إضافة حالات اختبار جديدة، كما تجعل الكود أسهل قراءته وصيانته أيضاً.
// 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)
}
})
}
}
-
استخدام الـ Mocks في الاختبار
الـ Mocking هو تقنية اختبار تشمل استبدال الاعتمادات لاختبار أجزاء مختلفة من الوظائف. في جو، الواجهات هي الطريقة الرئيسية لتنفيذ الـ Mocks. من خلال استخدام الواجهات، يمكن إنشاء تنفيذ وهمي واستخدامه في الاختبار.