الشيفرة موضع الاختبار
أدوات الاختبار في Go جزء من الأدوات القياسية: الحزمة testing والأمر go test. لا إطار عمل لتثبيته، ولا حاجة لمكتبة تأكيدات.
تختبر أمثلة هذه الصفحة دالة صغيرة واحدة، Slugify، تحوّل عنوانًا إلى slug لعنوان URL. ها هي قابلة للتشغيل، مع فحص مكتوب يدويًا في main:
يشغّل محرّر المتصفّح main؛ ولا يستطيع تشغيل go test. كل ما يلي مكتوب كما يعيش في مشروع حقيقي، مع مخرجات الطرفية بعد كل أمر. وقد شُغّل كله بـ Go 1.24.
الاختبار الأول
في المشروع تعيش الدالة في حزمة، وتعيش اختباراتها بجوارها في ملف ينتهي اسمه بـ _test.go:
textutil/
├── go.mod module example.com/textutil
├── slug.go package textutil, func Slugify
└── slug_test.go package textutil, the tests
// slug_test.go
package textutil
import "testing"
func TestSlugify(t *testing.T) {
got := Slugify("Hello, World!")
want := "hello-world"
if got != want {
t.Errorf("Slugify(%q) = %q, want %q", "Hello, World!", got, want)
}
}
القواعد التي يستخدمها go test:
- الملفات التي تنتهي بـ
_test.goوحدها ملفات اختبار. يتجاهلهاgo build، فلا تنتهي شيفرة الاختبار أبدًا في ملفك التنفيذي. - الاختبار دالة اسمها
TestXxx(الجزء الذي بعدTestيجب ألا يبدأ بحرف صغير) تأخذ*testing.Tواحدًا. - ينجح الاختبار ما لم يستدعِ إحدى دوال الفشل أو يسبّب panic.
go test # the package in the current directory
go test ./... # every package in the module
$ go test
PASS
ok example.com/textutil 0.318s
صيغة رسالة الفشل Func(input) = got, want expected هي اصطلاح Go. ضع المدخل في الرسالة: الفشل الذي يقول فقط got "a", want "b" يعيدك إلى الشيفرة لتعرف أي مدخل كان.
t.Errorf مقابل t.Fatalf
| الدالة | تعلّم بالفشل | توقف الاختبار |
|---|---|---|
t.Error، t.Errorf | نعم | لا، يستمر في العمل |
t.Fatal، t.Fatalf | نعم | نعم، فورًا |
t.Log، t.Logf | لا | لا، تطبع فقط مع -v أو عند الفشل |
t.Skip، t.Skipf | لا | نعم، ويُبلَّغ عنه كمتخطّى |
استخدم Errorf افتراضيًا، ليبلغ تشغيل واحد عن كل حقل خاطئ. واستخدم Fatalf عندما لا يمكن لبقية الاختبار أن تعمل، عادة بعد خطأ غير متوقّع:
func TestCountWords(t *testing.T) {
path := writeFile(t, "doc.txt", "the quick brown\nfox jumps\n")
got, err := CountWords(path)
if err != nil {
t.Fatalf("CountWords: unexpected error: %v", err) // stop: got is meaningless
}
if got != 5 {
t.Errorf("CountWords = %d, want 5", got)
}
}
توقف Fatal الاختبار باستدعاء runtime.Goexit، فيجب استدعاؤها من goroutine الاختبار نفسها. ومن goroutine بدأتها أنت أبلغ بـ t.Error وعُد.
اختبارات الجداول مع t.Run
معظم اختبارات Go جداول: شريحة من الحالات، وحلقة واحدة، وt.Run لتعطي كل حالة اسمها الخاص.
func TestSlugifyTable(t *testing.T) {
tests := []struct {
name, in, want string
}{
{"simple", "Go Testing", "go-testing"},
{"punctuation", "What's new in Go 1.24?", "what-s-new-in-go-1-24"},
{"extra spaces", " lots of space ", "lots-of-space"},
{"non-ascii dropped", "Café au lait", "caf-au-lait"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Slugify(tt.in); got != tt.want {
t.Errorf("Slugify(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
إضافة حالة سطر واحد. يُبلَّغ عن كل اختبار فرعي على حدة، وFatal داخل أحدها تنهي ذلك الاختبار الفرعي وحده، ويمكنك تشغيل واحد منها باسمه. شغّل كل شيء مع -v:
$ go test -v
=== RUN TestSlugify
--- PASS: TestSlugify (0.00s)
=== RUN TestSlugifyTable
=== RUN TestSlugifyTable/simple
=== RUN TestSlugifyTable/punctuation
=== RUN TestSlugifyTable/extra_spaces
=== RUN TestSlugifyTable/non-ascii_dropped
=== RUN TestSlugifyTable/empty
--- PASS: TestSlugifyTable (0.00s)
--- PASS: TestSlugifyTable/simple (0.00s)
--- PASS: TestSlugifyTable/punctuation (0.00s)
--- PASS: TestSlugifyTable/extra_spaces (0.00s)
--- PASS: TestSlugifyTable/non-ascii_dropped (0.00s)
--- PASS: TestSlugifyTable/empty (0.00s)
=== RUN TestCountWords
--- PASS: TestCountWords (0.00s)
=== RUN TestCountWordsMissingFile
--- PASS: TestCountWordsMissingFile (0.00s)
=== RUN ExampleSlugify
--- PASS: ExampleSlugify (0.00s)
PASS
ok example.com/textutil 0.182s
المسافات في أسماء الاختبارات الفرعية تصير شرطات سفلية. وعندما تفشل حالة تسمّي المخرجات الاختبار الفرعي والسطر:
--- FAIL: TestSlugifyTable (0.00s)
--- FAIL: TestSlugifyTable/underscore (0.00s)
slug_test.go:27: Slugify("snake_case_name") = "snake-case-name", want "snake_case_name"
FAIL
FAIL example.com/textutil 0.289s
منذ Go 1.22 لكل تكرار في الحلقة tt الخاص به، فترى الإغلاقات داخل t.Run الحالة الصحيحة حتى عندما تعمل الاختبارات الفرعية بالتوازي. الشيفرة الأقدم تحتوي غالبًا tt := tt في أول جسم الحلقة لهذا السبب؛ ولم يعد ذلك ضروريًا.
لتشغيل الاختبارات الفرعية بالتوازي استدعِ t.Parallel() في بداية دالة الاختبار الفرعي. افعل ذلك فقط عندما تكون الحالات مستقلّة وبطيئة بما يكفي ليُحدث ذلك فرقًا.
خيارات go test التي ستستخدمها
| الأمر | يفعل |
|---|---|
go test -v | يطبع اسم كل اختبار ونتيجته ومخرجات t.Log |
go test -run TestSlugify | يشغّل الاختبارات التي تطابق أسماؤها التعبير النمطي |
go test -run 'TestSlugifyTable/empty' | يشغّل اختبارًا فرعيًا واحدًا |
go test -count=1 | يتجاهل النتائج المخزّنة ويشغّل مجددًا |
go test -race | يشغّل مع كاشف سباقات البيانات |
go test -short | يخبر الاختبارات الطويلة بأن تتخطّى نفسها (if testing.Short() { t.Skip() }) |
go test -failfast | يتوقّف بعد أول اختبار فاشل |
go test -timeout 30s | يفشل إذا طال التشغيل أكثر (الافتراضي 10 دقائق) |
go test -cover | يطبع تغطية العبارات |
go test -bench=. | يشغّل اختبارات قياس الأداء أيضًا |
عندما تسمّي الحزم (go test . أو go test ./...) يخزّن go test نتائج الحزم التي لم تتغيّر شيفرتها ومدخلاتها ويطبع (cached) بعدها. أما go test دون وسائط فلا يخزّن أبدًا. تتتبّع الذاكرة المؤقّتة الملفات ومتغيّرات البيئة التي يقرؤها الاختبار عبر الحزمة os، لكنها لا تتتبّع الحالة الخارجية مثل قاعدة بيانات أو خدمة شبكة، فقد ينجح اختبار يعتمد على إحداها من الذاكرة المؤقّتة بينما كان سيفشل الآن؛ ويفرض -count=1 تشغيلًا حقيقيًا.
الدوال المساعدة والمجلدات المؤقّتة والتنظيف
استدعاء writeFile في TestCountWords السابق دالة مساعدة للاختبار:
// writeFile is a test helper: t.Helper makes failures point at the caller.
func writeFile(t *testing.T, name, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name) // removed automatically after the test
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("writing %s: %v", name, err)
}
return path
}
- تعلّم
t.Helper()الدالة كدالة مساعدة، فيُبلَّغ عن الفشل في سطر الاختبار الذي استدعاها، لا داخل الدالة المساعدة. - تنشئ
t.TempDir()مجلدًا جديدًا وتحذفه عند انتهاء الاختبار. لا تحتاج الاختبارات أبدًا إلى تنظيف الملفات بنفسها. - تسجّل
t.Cleanup(func() { ... })أي تفكيك آخر (إغلاق خادم، أو حذف جدول). تُنفَّذ عمليات التنظيف بعد الاختبار واختباراته الفرعية، والأخير تسجيلًا أولًا. - تضبط
t.Setenv("KEY", "value")متغيّر بيئة لهذا الاختبار وحده وتستعيده بعده. - تعيد
t.Context()(Go 1.24) سياقًا يُلغى قبل تنفيذ عمليات التنظيف مباشرة.
توضع ملفات البيانات الثابتة للاختبار في مجلد اسمه testdata بجوار الاختبارات. تتجاهله أداة go كحزمة، وتعمل الاختبارات ومجلد عملها هو مجلد الحزمة، فتعمل os.ReadFile("testdata/input.json").
اختبار الأخطاء ومعالجات HTTP
افحص الأخطاء بـ errors.Is أو errors.As، بالطريقة نفسها التي ستفعلها الشيفرة المستدعية:
func TestCountWordsMissingFile(t *testing.T) {
_, err := CountWords(filepath.Join(t.TempDir(), "nope.txt"))
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("err = %v, want fs.ErrNotExist", err)
}
}
مقارنة نصوص الأخطاء تنكسر حالما يعيد أحدهم صياغة رسالة.
لمعالجات HTTP تعطيك net/http/httptest كائن ResponseWriter وهميًا فتستدعي المعالج مباشرة، دون شبكة:
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
if body := rec.Body.String(); body != "ok\n" {
t.Errorf("body = %q, want %q", body, "ok\n")
}
}
تشغّل httptest.NewServer خادمًا حقيقيًا على localhost لاختبار شيفرة العميل. وتستخدمها صفحة عميل HTTP في كل مثال.
التغطية
$ go test -cover
PASS
coverage: 100.0% of statements
ok example.com/textutil 0.578s
$ go test -coverprofile=cover.out
$ go tool cover -func=cover.out
example.com/textutil/slug.go:11: Slugify 100.0%
example.com/textutil/words.go:9: CountWords 100.0%
total: (statements) 100.0%
$ go tool cover -html=cover.out # opens a browser with covered lines in green
تخبرك التغطية بالأسطر التي لم تُنفَّذ قط، وهذا مفيد لإيجاد الفروع غير المختبرة. لكن الرقم العالي لا يخبرك أن التأكيدات جيدة: الاختبار الذي يستدعي كل دالة ولا يفحص شيئًا يصل إلى 100%.
قياس الأداء
اختبار قياس الأداء هو func BenchmarkXxx(b *testing.B). أضافت Go 1.24 الدالة b.Loop، وهي الشكل الموصى به الآن:
func BenchmarkSlugify(b *testing.B) {
for b.Loop() {
Slugify("The Go Programming Language, 2nd Edition")
}
}
تشغّل b.Loop الجسم بعدد المرات اللازم لقياس مستقر، وتستبعد شيفرة الإعداد قبل الحلقة من التوقيت، وتمنع المترجم من حذف الاستدعاء بالتحسين. الشيفرة المكتوبة قبل Go 1.24 تستخدم for i := 0; i < b.N; i++، التي ما زالت تعمل لكنها تحتاج b.ResetTimer() بعد الإعداد المكلف وقد يخدعها حذف الشيفرة الميتة.
لا تعمل اختبارات قياس الأداء مع go test العادي. اطلبها، وتخطَّ الاختبارات العادية بـ -run='^$':
$ go test -bench=. -benchmem -run='^$'
goos: darwin
goarch: arm64
pkg: example.com/textutil
cpu: Apple M4
BenchmarkSlugify-10 5061945 238.1 ns/op 168 B/op 5 allocs/op
PASS
ok example.com/textutil 1.410s
الأعمدة هي: الاسم ملحقًا به GOMAXPROCS، وعدد التكرارات المنفّذة، والوقت لكل استدعاء، والبايتات المحجوزة لكل استدعاء، وعمليات الحجز لكل استدعاء. تعتمد الأرقام على الجهاز؛ قارن التشغيلات على الجهاز نفسه. ولمقارنة ما قبل التغيير وما بعده بموثوقية، شغّل كل طرف عدة مرات بـ -count=10 ومرّر المخرجات كلتيهما إلى benchstat (golang.org/x/perf/cmd/benchstat).
الأمثلة اختبارات أيضًا
دالة المثال تطبع شيئًا وتعلن المخرجات المتوقّعة في تعليق. يشغّلها go test ويفشل إذا اختلفت المخرجات، ويعرضها go doc وpkg.go.dev كتوثيق:
// example_test.go
package textutil_test
import (
"fmt"
"example.com/textutil"
)
func ExampleSlugify() {
fmt.Println(textutil.Slugify("Hello, World!"))
// Output: hello-world
}
اسم الحزمة textutil_test يجعل هذا اختبارًا خارجيًا: لا يستطيع إلا استخدام الواجهة البرمجية المُصدَّرة، كما يفعل مستدعٍ حقيقي. ويمكن لهذه الملفات أن تكون في مجلد الحزمة نفسه. دون تعليق // Output: يُترجم المثال لكنه لا يُشغَّل. استخدم // Unordered output: عندما يمكن أن تظهر الأسطر بأي ترتيب.
الاختبار العشوائي (fuzzing) باختصار
أضافت Go 1.18 اختبارات fuzz، التي تولّد مدخلات لإيجاد الانهيارات والثوابت المكسورة:
func FuzzSlugify(f *testing.F) {
f.Add("Hello, World!") // seed input
f.Fuzz(func(t *testing.T, s string) {
slug := Slugify(s)
if strings.Contains(slug, "--") || strings.HasPrefix(slug, "-") {
t.Errorf("Slugify(%q) = %q: bad hyphens", s, slug)
}
})
}
يشغّل go test العادي مدخلات البذور وحدها (قيم f.Add وأي ملفات محفوظة تحت testdata/fuzz). ويستمر go test -fuzz=FuzzSlugify في توليد مدخلات جديدة حتى يجد فشلًا أو توقفه، ويحفظ المدخلات الفاشلة تحت testdata/fuzz فتصير حالات اختبار عادية.
أخطاء شائعة
- ملف اختبار في مكان خاطئ أو باسم خاطئ. يجب أن ينتهي بـ
_test.goوأن يكون في مجلد الحزمة. أماslug_tests.goفيُترجم ضمن الحزمة، ولا يُشغَّل كاختبارات. - حرف صغير بعد
Test.Testslugifyليس اختبارًا. أماTestSlugifyوTest_slugifyفهما اختباران. t.Fatalمن goroutine أخرى. أبلغ بـt.Errorهناك، وانتظر الـ goroutine قبل أن يعود الاختبار.- رسائل دون المدخل. ضمّن ما مُرّر، وما خرج، وما كان متوقّعًا.
- الثقة بنجاح مخزّن. استخدم
-count=1عندما يعتمد اختبار على أي شيء خارج الحزمة. - اختبارات تعتمد على ترتيب بعضها أو على متغيّرات عامة مشتركة. يجب أن يجهّز كل اختبار حالته الخاصة، وهذا ما وُجدت له
t.TempDirوt.Setenvوt.Cleanup.
الأسئلة الشائعة
كيف أكتب اختبار وحدة في Go؟
أنشئ ملفًا ينتهي بـ _test.go في مجلد الشيفرة نفسه، واستورد testing، واكتب دالة func TestName(t *testing.T) تستدعي شيفرتك وتبلغ عن عدم التطابق بـ t.Errorf. شغّل go test في ذلك المجلد، أو go test ./... للوحدة كلها. لا حاجة لإطار عمل أو مكتبة تأكيدات.
ما الفرق بين t.Error وt.Fatal في Go؟
تعلّم t.Error وt.Errorf الاختبار بالفشل وتستمران في تشغيله، فيبلغ تشغيل واحد عن عدة مشكلات. أما t.Fatal وt.Fatalf فتعلّمانه بالفشل وتوقفان الاختبار فورًا. استخدم Fatal عندما لا يكون للمتابعة معنى، مثل بعد خطأ غير متوقّع يترك النتيجة غير قابلة للاستخدام.
كيف أشغّل اختبارًا واحدًا في Go؟
مرّر تعبيرًا نمطيًا إلى -run: يشغّل go test -run TestSlugify كل اختبار يطابق اسمه، ويشغّل go test -run 'TestSlugifyTable/punctuation' اختبارًا فرعيًا واحدًا (المسافات في أسماء الاختبارات الفرعية تصير شرطات سفلية). أضف -v لترى اسم كل اختبار ونتيجته، و-count=1 لتجاوز ذاكرة الاختبارات المؤقّتة.
كيف أكتب اختبار قياس أداء (benchmark) في Go؟
اكتب func BenchmarkName(b *testing.B) في ملف _test.go وضع الشيفرة المراد قياسها في حلقة for b.Loop() { ... } (Go 1.24؛ الشيفرة الأقدم تستخدم for i := 0; i < b.N; i++). شغّله بـ go test -bench=. -benchmem، الذي يبلغ عن النانوثواني والبايتات وعمليات الحجز لكل عملية.
كيف أرى تغطية الاختبارات في Go؟
يطبع go test -cover نسبة العبارات التي نفّذتها الاختبارات. وللتفاصيل اكتب ملف تعريف بـ go test -coverprofile=cover.out، ثم شغّل go tool cover -func=cover.out للأرقام لكل دالة أو go tool cover -html=cover.out لترى الأسطر المغطّاة وغير المغطّاة في المتصفّح.