Let's Go تست › تست فرم‌های HTML
قبلی · فهرست · بعدی
فصل 13.6.

تست فرم‌های HTML (Testing HTML Forms)

در این بخش، نحوه تست فرم‌های HTML (Testing HTML Forms) را بررسی می‌کنیم. این شامل ارسال فرم (Form Submission) و اعتبارسنجی داده‌ها (Data Validation) می‌شود.

همچنین با توکن CSRF (CSRF Token) و پیام‌های خطا (Error Messages) آشنا خواهیم شد.

در این فصل، ما قصد داریم یک تست انتها به انتها برای مسیر POST /user/signup اضافه کنیم، که توسط هندلر userSignupPost مدیریت می‌شود.

تست این مسیر به دلیل بررسی ضد-CSRF که برنامه ما انجام می‌دهد، کمی پیچیده‌تر است. هر درخواستی که به POST /user/signup ارسال می‌شود، همیشه یک پاسخ 400 Bad Request دریافت می‌کند مگر اینکه درخواست شامل یک توکن CSRF معتبر و کوکی باشد. برای دور زدن این موضوع، باید به عنوان بخشی از تست خود، جریان کاری یک کاربر واقعی را شبیه‌سازی کنیم، به این صورت:

  1. یک درخواست GET /user/signup ارسال کنید. این درخواست پاسخی را برمی‌گرداند که شامل یک کوکی CSRF در هدرهای پاسخ و توکن CSRF برای صفحه ثبت‌نام در بدنه پاسخ است.

  2. توکن CSRF را از بدنه پاسخ HTML استخراج کنید.

  3. یک درخواست POST /user/signup ارسال کنید، با استفاده از همان http.Client که در مرحله 1 استفاده کردیم (بنابراین به طور خودکار کوکی CSRF را با درخواست POST ارسال می‌کند) و شامل توکن CSRF به همراه سایر داده‌های POST که می‌خواهیم تست کنیم.

بیایید با اضافه کردن یک تابع کمکی جدید به فایل cmd/web/testutils_test.go برای استخراج توکن CSRF (در صورت وجود) از بدنه پاسخ HTML شروع کنیم:

File: cmd/web/testutils_test.go
package main

import (
    "bytes"
    "html" // New import
    "io"
    "log/slog"
    "net/http"
    "net/http/cookiejar"
    "net/http/httptest"
    "regexp" // New import
    "testing"
    "time"

    "snippetbox.alexedwards.net/internal/models/mocks"

    "github.com/alexedwards/scs/v2"
    "github.com/go-playground/form/v4"
)

// Define a regular expression which captures the CSRF token value from the
// HTML for our user signup page.
var csrfTokenRX = regexp.MustCompile(`<input type='hidden' name='csrf_token' value='(.+)'>`)

func extractCSRFToken(t *testing.T, body string) string {
    // Use the FindStringSubmatch method to extract the token from the HTML body.
    // Note that this returns an array with the entire matched pattern in the
    // first position, and the values of any captured data in the subsequent
    // positions.
    matches := csrfTokenRX.FindStringSubmatch(body)
    if len(matches) < 2 {
        t.Fatal("no csrf token found in body")
    }

    return html.UnescapeString(matches[1])
}

...

حالا که این در جای خود قرار دارد، بیایید به فایل cmd/web/handlers_test.go برگردیم و یک تست جدید TestUserSignup ایجاد کنیم.

برای شروع، ما این کار را با ارسال یک درخواست GET /user/signup و سپس استخراج و چاپ توکن CSRF از بدنه پاسخ HTML انجام خواهیم داد. به این صورت:

File: cmd/web/handlers_test.go
package main

...

func TestUserSignup(t *testing.T) {
    // Create the application struct containing our mocked dependencies and set
    // up the test server for running an end-to-end test.
    app := newTestApplication(t)
    ts := newTestServer(t, app.routes())
    defer ts.Close()

    // Make a GET /user/signup request and then extract the CSRF token from the
    // response body.
    _, _, body := ts.get(t, "/user/signup")
    csrfToken := extractCSRFToken(t, body)

    // Log the CSRF token value in our test output using the t.Logf() function. 
    // The t.Logf() function works in the same way as fmt.Printf(), but writes 
    // the provided message to the test output.
    t.Logf("CSRF token is: %q", csrfToken)
}

مهم است که تست‌ها را با استفاده از فلگ -v (برای فعال کردن خروجی مفصل) اجرا کنید تا هر خروجی از تابع t.Logf() را ببینید.

بیایید این کار را اکنون انجام دهیم:

$ go test -v -run="TestUserSignup" ./cmd/web/
=== RUN   TestUserSignup
    handlers_test.go:81: CSRF token is: "C92tcpQpL1n6aIUaF8XAonwy+YjcVnyaAaOvfkdl6vJqoNSbgaTtdBRC61pFMoGP2ojV+sZ1d0SUikah3mfREQ=="
--- PASS: TestUserSignup (0.01s)
PASS
ok      snippetbox.alexedwards.net/cmd/web      0.010s

خوب، به نظر می‌رسد که کار می‌کند. تست بدون هیچ مشکلی اجرا می‌شود و توکن CSRF که از بدنه پاسخ استخراج کرده‌ایم را چاپ می‌کند.

تست درخواست‌های پست

حالا بیایید به فایل cmd/web/testutils_test.go برگردیم و یک متد جدید postForm() روی نوع testServer خود ایجاد کنیم، که می‌توانیم از آن برای ارسال یک درخواست POST به سرور تست خود با داده‌های فرم خاص در بدنه درخواست استفاده کنیم.

کد زیر را اضافه کنید (که از همان الگوی کلی که برای متد get() قبلاً در کتاب استفاده کردیم پیروی می‌کند):

File: cmd/web/testutils_test.go
package main

import (
    "bytes"
    "html"
    "io"
    "log/slog"
    "net/http"
    "net/http/cookiejar"
    "net/http/httptest"
    "net/url" // New import
    "regexp"
    "testing"
    "time"

    "snippetbox.alexedwards.net/internal/models/mocks"

    "github.com/alexedwards/scs/v2"
    "github.com/go-playground/form/v4"
)

...

// Create a postForm method for sending POST requests to the test server. The
// final parameter to this method is a url.Values object which can contain any
// form data that you want to send in the request body.
func (ts *testServer) postForm(t *testing.T, urlPath string, form url.Values) (int, http.Header, string) {
    rs, err := ts.Client().PostForm(ts.URL+urlPath, form)
    if err != nil {
        t.Fatal(err)
    }

    // Read the response body from the test server.
    defer rs.Body.Close()
    body, err := io.ReadAll(rs.Body)
    if err != nil {
        t.Fatal(err)
    }
    body = bytes.TrimSpace(body)

    // Return the response status, headers and body.
    return rs.StatusCode, rs.Header, string(body)
}

و حالا، در نهایت، آماده‌ایم تا برخی از تست‌های زیرمجموعه‌ای مبتنی بر جدول را برای تست رفتار مسیر POST /user/signup برنامه خود اضافه کنیم. به طور خاص، می‌خواهیم تست کنیم که:

به‌روزرسانی تابع TestUserSignup برای انجام این تست‌ها به این صورت:

File: cmd/web/handlers_test.go
package main

import (
    "net/http"
    "net/url" // New import
    "testing"

    "snippetbox.alexedwards.net/internal/assert"
)

...

func TestUserSignup(t *testing.T) {
    app := newTestApplication(t)
    ts := newTestServer(t, app.routes())
    defer ts.Close()

    _, _, body := ts.get(t, "/user/signup")
    validCSRFToken := extractCSRFToken(t, body)

    const (
        validName     = "Bob"
        validPassword = "validPa$$word"
        validEmail    = "bob@example.com"
        formTag       = "<form action='/user/signup' method='POST' novalidate>"
    )

    tests := []struct {
        name         string
        userName     string
        userEmail    string
        userPassword string
        csrfToken    string
        wantCode     int
        wantFormTag  string
    }{
        {
            name:         "Valid submission",
            userName:     validName,
            userEmail:    validEmail,
            userPassword: validPassword,
            csrfToken:    validCSRFToken,
            wantCode:     http.StatusSeeOther,
        },
        {
            name:         "Invalid CSRF Token",
            userName:     validName,
            userEmail:    validEmail,
            userPassword: validPassword,
            csrfToken:    "wrongToken",
            wantCode:     http.StatusBadRequest,
        },
        {
            name:         "Empty name",
            userName:     "",
            userEmail:    validEmail,
            userPassword: validPassword,
            csrfToken:    validCSRFToken,
            wantCode:     http.StatusUnprocessableEntity,
            wantFormTag:  formTag,
        },
        {
            name:         "Empty email",
            userName:     validName,
            userEmail:    "",
            userPassword: validPassword,
            csrfToken:    validCSRFToken,
            wantCode:     http.StatusUnprocessableEntity,
            wantFormTag:  formTag,
        },
        {
            name:         "Empty password",
            userName:     validName,
            userEmail:    validEmail,
            userPassword: "",
            csrfToken:    validCSRFToken,
            wantCode:     http.StatusUnprocessableEntity,
            wantFormTag:  formTag,
        },
        {
            name:         "Invalid email",
            userName:     validName,
            userEmail:    "bob@example.",
            userPassword: validPassword,
            csrfToken:    validCSRFToken,
            wantCode:     http.StatusUnprocessableEntity,
            wantFormTag:  formTag,
        },
        {
            name:         "Short password",
            userName:     validName,
            userEmail:    validEmail,
            userPassword: "pa$$",
            csrfToken:    validCSRFToken,
            wantCode:     http.StatusUnprocessableEntity,
            wantFormTag:  formTag,
        },
        {
            name:         "Duplicate email",
            userName:     validName,
            userEmail:    "dupe@example.com",
            userPassword: validPassword,
            csrfToken:    validCSRFToken,
            wantCode:     http.StatusUnprocessableEntity,
            wantFormTag:  formTag,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            form := url.Values{}
            form.Add("name", tt.userName)
            form.Add("email", tt.userEmail)
            form.Add("password", tt.userPassword)
            form.Add("csrf_token", tt.csrfToken)

            code, _, body := ts.postForm(t, "/user/signup", form)

            assert.Equal(t, code, tt.wantCode)

            if tt.wantFormTag != "" {
                assert.StringContains(t, body, tt.wantFormTag)
            }
        })
    }
}

اگر تست را اجرا کنید، باید ببینید که همه زیرمجموعه‌های تست اجرا و با موفقیت پاس می‌شوند — مشابه این:

$ go test -v -run="TestUserSignup" ./cmd/web/
=== RUN   TestUserSignup
=== RUN   TestUserSignup/Valid_submission
=== RUN   TestUserSignup/Invalid_CSRF_Token
=== RUN   TestUserSignup/Empty_name
=== RUN   TestUserSignup/Empty_email
=== RUN   TestUserSignup/Empty_password
=== RUN   TestUserSignup/Invalid_email
=== RUN   TestUserSignup/Short_password
=== RUN   TestUserSignup/Long_password
=== RUN   TestUserSignup/Duplicate_email
--- PASS: TestUserSignup (0.01s)
    --- PASS: TestUserSignup/Valid_submission (0.00s)
    --- PASS: TestUserSignup/Invalid_CSRF_Token (0.00s)
    --- PASS: TestUserSignup/Empty_name (0.00s)
    --- PASS: TestUserSignup/Empty_email (0.00s)
    --- PASS: TestUserSignup/Empty_password (0.00s)
    --- PASS: TestUserSignup/Invalid_email (0.00s)
    --- PASS: TestUserSignup/Short_password (0.00s)
    --- PASS: TestUserSignup/Long_password (0.00s)
    --- PASS: TestUserSignup/Duplicate_email (0.00s)
PASS
ok      snippetbox.alexedwards.net/cmd/web      0.016s

واژه‌نامه اصطلاحات فنی

اصطلاح فارسی معادل انگلیسی توضیح
تست فرم‌های HTML Testing HTML Forms آزمایش فرم‌های وب
ارسال فرم Form Submission ارسال داده‌های فرم
اعتبارسنجی داده‌ها Data Validation بررسی صحت داده‌ها
توکن CSRF CSRF Token کد امنیتی فرم
پیام‌های خطا Error Messages اعلان‌های خطا
فیلدهای فرم Form Fields ورودی‌های فرم
درخواست POST POST Request ارسال داده به سرور
هدرهای HTTP HTTP Headers سرآیندهای درخواست
کوکی‌ها Cookies داده‌های ذخیره شده
بازخورد کاربر User Feedback پاسخ به کاربر