تست فرمهای 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 معتبر و کوکی باشد. برای دور زدن این موضوع، باید به عنوان بخشی از تست خود، جریان کاری یک کاربر واقعی را شبیهسازی کنیم، به این صورت:
یک درخواست
GET /user/signupارسال کنید. این درخواست پاسخی را برمیگرداند که شامل یک کوکی CSRF در هدرهای پاسخ و توکن CSRF برای صفحه ثبتنام در بدنه پاسخ است.توکن CSRF را از بدنه پاسخ HTML استخراج کنید.
یک درخواست
POST /user/signupارسال کنید، با استفاده از همانhttp.Clientکه در مرحله 1 استفاده کردیم (بنابراین به طور خودکار کوکی CSRF را با درخواستPOSTارسال میکند) و شامل توکن CSRF به همراه سایر دادههایPOSTکه میخواهیم تست کنیم.
بیایید با اضافه کردن یک تابع کمکی جدید به فایل cmd/web/testutils_test.go برای استخراج توکن CSRF (در صورت وجود) از بدنه پاسخ HTML شروع کنیم:
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 انجام خواهیم داد. به این صورت:
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() قبلاً در کتاب استفاده کردیم پیروی میکند):
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 برنامه خود اضافه کنیم. به طور خاص، میخواهیم تست کنیم که:
- یک ثبتنام معتبر منجر به پاسخ
303 See Otherمیشود. - ارسال فرم بدون توکن CSRF معتبر منجر به پاسخ
400 Bad Requestمیشود. - ارسال فرم نامعتبر منجر به پاسخ
422 Unprocessable Entityمیشود و فرم ثبتنام دوباره نمایش داده میشود. این باید زمانی اتفاق بیفتد که:- فیلدهای نام، ایمیل یا رمز عبور خالی باشند.
- ایمیل در قالب معتبری نباشد.
- رمز عبور کمتر از 8 کاراکتر باشد.
- آدرس ایمیل قبلاً استفاده شده باشد.
بهروزرسانی تابع TestUserSignup برای انجام این تستها به این صورت:
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 | پاسخ به کاربر |