Let's Go تست › تست هندلرها و میان‌افزارهای HTTP
قبلی · فهرست · بعدی
فصل 13.2.

تست هندلرها و میان‌افزارهای 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" برمی‌گرداند. این نوع هندلری است که ممکن است بخواهید برای بررسی وضعیت یا نظارت بر زمان کارکرد سرور خود پیاده‌سازی کنید.

File: cmd/web/handlers.go
package main

...

func ping(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("OK"))
}

در این فصل یک تست واحد TestPing جدید ایجاد خواهیم کرد که:

ضبط پاسخ‌ها

Go ابزارهای مفیدی در بسته net/http/httptest برای کمک به تست هندلرهای HTTP شما فراهم می‌کند.

یکی از این ابزارها نوع httptest.ResponseRecorder است. این اساساً یک پیاده‌سازی از http.ResponseWriter است که کد وضعیت پاسخ، هدرها و بدنه را ضبط می‌کند به جای اینکه واقعاً آنها را به یک اتصال HTTP بنویسد.

بنابراین یک راه آسان برای تست واحد هندلرهای شما این است که یک httptest.ResponseRecorder جدید ایجاد کنید، آن را به تابع هندلر پاس دهید و سپس بعد از بازگشت هندلر آن را دوباره بررسی کنید.

بیایید دقیقاً همین کار را برای تست تابع هندلر ping انجام دهیم.

ابتدا، طبق کنوانسیون‌های Go، یک فایل handlers_test.go جدید ایجاد کنید تا تست را نگه دارد…

$ touch cmd/web/handlers_test.go

و سپس کد زیر را اضافه کنید:

File: 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() که قبلاً در کتاب ساخته‌ایم، نشان خواهیم داد. به عنوان بخشی از این تست می‌خواهیم بررسی کنیم که:

ابتدا باید یک فایل cmd/web/middleware_test.go ایجاد کنید تا تست را نگه دارد:

$ touch cmd/web/middleware_test.go

و سپس کد زیر را اضافه کنید:

File: 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