تست هندلرها و میانافزارهای HTTP (Testing HTTP Handlers and Middleware)
در این بخش، نحوه تست هندلرهای HTTP (Testing HTTP Handlers) و میانافزارها (Middleware) را بررسی میکنیم. این شامل درخواستهای تست (Test Requests) و پاسخهای تست (Test Responses) میشود.
همچنین با شبیهسازی درخواست (Request Mocking) و بررسی پاسخ (Response Verification) آشنا خواهیم شد.
بیایید به سراغ تکنیکهای خاصی برای تست واحد هندلرهای HTTP شما برویم.
تمام هندلرهایی که تا کنون برای این پروژه نوشتهایم کمی پیچیده برای تست هستند و برای معرفی چیزها ترجیح میدهم با چیزی سادهتر شروع کنم.
بنابراین، اگر همراه هستید، به فایل handlers.go خود بروید و یک تابع هندلر ping جدید ایجاد کنید که یک کد وضعیت 200 OK و یک بدنه پاسخ "OK" برمیگرداند. این نوع هندلری است که ممکن است بخواهید برای بررسی وضعیت یا نظارت بر زمان کارکرد سرور خود پیادهسازی کنید.
package main ... func ping(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
در این فصل یک تست واحد TestPing جدید ایجاد خواهیم کرد که:
- بررسی میکند که کد وضعیت پاسخ نوشته شده توسط هندلر
ping200است. - بررسی میکند که بدنه پاسخ نوشته شده توسط هندلر
ping"OK"است.
ضبط پاسخها
Go ابزارهای مفیدی در بسته net/http/httptest برای کمک به تست هندلرهای HTTP شما فراهم میکند.
یکی از این ابزارها نوع httptest.ResponseRecorder است. این اساساً یک پیادهسازی از http.ResponseWriter است که کد وضعیت پاسخ، هدرها و بدنه را ضبط میکند به جای اینکه واقعاً آنها را به یک اتصال HTTP بنویسد.
بنابراین یک راه آسان برای تست واحد هندلرهای شما این است که یک httptest.ResponseRecorder جدید ایجاد کنید، آن را به تابع هندلر پاس دهید و سپس بعد از بازگشت هندلر آن را دوباره بررسی کنید.
بیایید دقیقاً همین کار را برای تست تابع هندلر ping انجام دهیم.
ابتدا، طبق کنوانسیونهای Go، یک فایل handlers_test.go جدید ایجاد کنید تا تست را نگه دارد…
$ touch cmd/web/handlers_test.go
و سپس کد زیر را اضافه کنید:
package main import ( "bytes" "io" "net/http" "net/http/httptest" "testing" "snippetbox.alexedwards.net/internal/assert" ) func TestPing(t *testing.T) { // Initialize a new httptest.ResponseRecorder. rr := httptest.NewRecorder() // Initialize a new dummy http.Request. r, err := http.NewRequest(http.MethodGet, "/", nil) if err != nil { t.Fatal(err) } // Call the ping handler function, passing in the // httptest.ResponseRecorder and http.Request. ping(rr, r) // Call the Result() method on the http.ResponseRecorder to get the // http.Response generated by the ping handler. rs := rr.Result() // Check that the status code written by the ping handler was 200. assert.Equal(t, rs.StatusCode, http.StatusOK) // And we can check that the response body written by the ping handler // equals "OK". defer rs.Body.Close() body, err := io.ReadAll(rs.Body) if err != nil { t.Fatal(err) } body = bytes.TrimSpace(body) assert.Equal(t, string(body), "OK") }
خوب، فایل را ذخیره کنید، سپس دوباره go test را با پرچم verbose اجرا کنید. به این صورت:
$ go test -v ./cmd/web/
=== RUN TestPing
--- PASS: TestPing (0.00s)
=== RUN TestHumanDate
=== RUN TestHumanDate/UTC
=== RUN TestHumanDate/Empty
=== RUN TestHumanDate/CET
--- PASS: TestHumanDate (0.00s)
--- PASS: TestHumanDate/UTC (0.00s)
--- PASS: TestHumanDate/Empty (0.00s)
--- PASS: TestHumanDate/CET (0.00s)
PASS
ok snippetbox.alexedwards.net/cmd/web 0.003s
بنابراین این خوب به نظر میرسد. میتوانیم ببینیم که تست جدید TestPing ما در حال اجرا است و بدون هیچ مشکلی پاس میشود.
تست میانافزار
همچنین میتوان از همان الگوی کلی برای تست واحد میانافزارهای خود استفاده کرد.
ما این را با ایجاد یک تست TestCommonHeaders برای میانافزار commonHeaders() که قبلاً در کتاب ساختهایم، نشان خواهیم داد. به عنوان بخشی از این تست میخواهیم بررسی کنیم که:
- میانافزار
commonHeaders()تمام هدرهای مورد انتظار را بر روی پاسخ HTTP تنظیم میکند. - میانافزار
commonHeaders()به درستی هندلر بعدی در زنجیره را فراخوانی میکند.
ابتدا باید یک فایل cmd/web/middleware_test.go ایجاد کنید تا تست را نگه دارد:
$ touch cmd/web/middleware_test.go
و سپس کد زیر را اضافه کنید:
package main import ( "bytes" "io" "net/http" "net/http/httptest" "testing" "snippetbox.alexedwards.net/internal/assert" ) func TestCommonHeaders(t *testing.T) { // Initialize a new httptest.ResponseRecorder and dummy http.Request. rr := httptest.NewRecorder() r, err := http.NewRequest(http.MethodGet, "/", nil) if err != nil { t.Fatal(err) } // Create a mock HTTP handler that we can pass to our commonHeaders // middleware, which writes a 200 status code and an "OK" response body. next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }) // Pass the mock HTTP handler to our commonHeaders middleware. Because // commonHeaders *returns* a http.Handler we can call its ServeHTTP() // method, passing in the http.ResponseRecorder and dummy http.Request to // execute it. commonHeaders(next).ServeHTTP(rr, r) // Call the Result() method on the http.ResponseRecorder to get the results // of the test. rs := rr.Result() // Check that the middleware has correctly set the Content-Security-Policy // header on the response. expectedValue := "default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com" assert.Equal(t, rs.Header.Get("Content-Security-Policy"), expectedValue) // Check that the middleware has correctly set the Referrer-Policy // header on the response. expectedValue = "origin-when-cross-origin" assert.Equal(t, rs.Header.Get("Referrer-Policy"), expectedValue) // Check that the middleware has correctly set the X-Content-Type-Options // header on the response. expectedValue = "nosniff" assert.Equal(t, rs.Header.Get("X-Content-Type-Options"), expectedValue) // Check that the middleware has correctly set the X-Frame-Options header // on the response. expectedValue = "deny" assert.Equal(t, rs.Header.Get("X-Frame-Options"), expectedValue) // Check that the middleware has correctly set the X-XSS-Protection header // on the response expectedValue = "0" assert.Equal(t, rs.Header.Get("X-XSS-Protection"), expectedValue) // Check that the middleware has correctly set the Server header on the // response. expectedValue = "Go" assert.Equal(t, rs.Header.Get("Server"), expectedValue) // Check that the middleware has correctly called the next handler in line // and the response status code and body are as expected. assert.Equal(t, rs.StatusCode, http.StatusOK) defer rs.Body.Close() body, err := io.ReadAll(rs.Body) if err != nil { t.Fatal(err) } body = bytes.TrimSpace(body) assert.Equal(t, string(body), "OK") }
اگر اکنون تستها را اجرا کنید، باید ببینید که تست TestCommonHeaders بدون هیچ مشکلی پاس میشود.
$ go test -v ./cmd/web/
=== RUN TestPing
--- PASS: TestPing (0.00s)
=== RUN TestCommonHeaders
--- PASS: TestCommonHeaders (0.00s)
=== RUN TestHumanDate
=== RUN TestHumanDate/UTC
=== RUN TestHumanDate/Empty
=== RUN TestHumanDate/CET
--- PASS: TestHumanDate (0.00s)
--- PASS: TestHumanDate/UTC (0.00s)
--- PASS: TestHumanDate/Empty (0.00s)
--- PASS: TestHumanDate/CET (0.00s)
PASS
ok snippetbox.alexedwards.net/cmd/web 0.003s
بنابراین، به طور خلاصه، یک راه سریع و آسان برای تست واحد هندلرها و میانافزارهای HTTP شما این است که به سادگی آنها را با استفاده از نوع httptest.ResponseRecorder فراخوانی کنید. سپس میتوانید کد وضعیت، هدرها و بدنه پاسخ ضبط شده را بررسی کنید تا مطمئن شوید که آنها به درستی کار میکنند.
واژهنامه اصطلاحات فنی
| اصطلاح فارسی | معادل انگلیسی | توضیح |
|---|---|---|
| تست هندلرهای HTTP | Testing HTTP Handlers | آزمایش توابع پردازش HTTP |
| میانافزارها | Middleware | کدهای واسط پردازش |
| درخواستهای تست | Test Requests | درخواستهای شبیهسازی شده |
| پاسخهای تست | Test Responses | پاسخهای مورد انتظار |
| شبیهسازی درخواست | Request Mocking | ایجاد درخواست مجازی |
| بررسی پاسخ | Response Verification | تأیید صحت پاسخ |
| سرآیندهای HTTP | HTTP Headers | اطلاعات اضافی درخواست |
| کد وضعیت | Status Code | کد نتیجه درخواست |
| بدنه پاسخ | Response Body | محتوای اصلی پاسخ |
| مسیر درخواست | Request Path | آدرس درخواست HTTP |