Let's Go تست › تست handlerها و middlewareهای HTTP
قبلی · فهرست · بعدی
فصل ۱۳.۲.

تست handlerها و middlewareهای HTTP

بیایید ادامه دهیم و برخی تکنیک‌های خاص برای تست واحد handlerهای HTTP خود را بحث کنیم.

همه handlerهایی که تا کنون برای این پروژه نوشته‌ایم کمی پیچیده برای تست هستند، و برای معرفی چیزها ترجیح می‌دهم با چیزی ساده‌تر شروع کنم.

پس، اگر همراه ما هستید، به فایل handlers.go خود بروید و یک تابع handler جدید ping ایجاد کنید که یک کد وضعیت 200 OK و یک بدنه پاسخ "OK" برمی‌گرداند. این نوع handlerی است که ممکن است بخواهید برای بررسی وضعیت (status-checking) یا نظارت بر uptime سرور خود پیاده‌سازی کنید.

File: cmd/web/handlers.go
package main

...

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

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

ثبت پاسخ‌ها

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

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

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

بیایید دقیقاً این کار را برای تست تابع handler 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 ما در حال اجرا است و بدون هیچ مشکلی پاس می‌شود.

تست middleware

همچنین می‌توان از همان الگوی کلی برای تست واحد middleware خود استفاده کرد.

این را با ایجاد یک تست جدید TestCommonHeaders برای middleware 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

پس، به طور خلاصه، یک راه سریع و آسان برای تست واحد handlerها و middlewareهای HTTP شما این است که به سادگی آن‌ها را با استفاده از نوع httptest.ResponseRecorder فراخوانی کنید. سپس می‌توانید کد وضعیت، headerها و بدنه پاسخ پاسخ ثبت شده را بررسی کنید تا مطمئن شوید که آن‌ها همان‌طور که انتظار می‌رود کار می‌کنند.