Creating a unit test
In Go, it’s standard practice to write your tests in *_test.go files which live directly alongside the code that you’re testing. So, in this case, the first thing that we’re going to do is create a new cmd/web/template_test.go file to hold the test.
And then we can create a new unit test for the humanDate function like so:
package main import ( "testing" "time" ) func TestHumanDate(t *testing.T) { // Initialize a new time.Time object and pass it to the humanDate function. tm := time.Date(2024, 3, 17, 10, 15, 0, 0, time.UTC) hd := humanDate(tm) // Check that the output from the humanDate function is in the format we expect. If it isn't // what we expect, use the t.Errorf() function to indicate that the test has failed and log // the expected and actual values. if hd != "17 Mar 2024 at 10:15" { t.Errorf("got %q; want %q", hd, "17 Mar 2024 at 10:15") } }
zzh@ZZHPC:/zdata/Github/snippetbox$ go test -v ./cmd/web === RUN TestHumanDate --- PASS: TestHumanDate (0.00s) PASS ok snippetbox/cmd/web 0.002s
Table-driven tests
Let’s now expand our TestHumanDate() function to cover some additional test cases. Specifically, we’re going to update it to also check that:
1. If the input to humanDate() is the zero time, then it returns the empty string "" .
2. The output from the humanDate() function always uses the UTC time zone.
In Go, an idiomatic way to run multiple test cases is to use table-driven tests.
Essentially, the idea behind table-driven tests is to create a ‘table’ of test cases containing the inputs and expected outputs, and to then loop over these, running each test case in a sub-test. There are a few ways you could set this up, but a common approach is to define your test cases in an slice of anonymous structs.
package main import ( "testing" "time" ) func TestHumanDate(t *testing.T) { // Create a slice of anonymous structs containing the test case name, input to our humanDate() // function, and expected output. tests := []struct{ name string input time.Time expected string }{ { name: "UTC", input: time.Date(2024, 3, 17, 10, 15, 0, 0, time.UTC), expected: "17 Mar 2024 at 10:15", }, { name: "Empty", input: time.Time{}, expected: "", }, { name: "CET", input: time.Date(2024, 3, 17, 10, 15, 0, 0, time.FixedZone("CET", 1*60*60)), expected: "17 Mar 2024 at 09:15", }, } // Loop over the test cases. for _, tc := range tests { // Use the t.Run() function to run a sub-test for each test case. The first parameter to // this is the name of the test (which is used to identify the sub-test in any log output) // and the second parameter is an anonymous function containing the actual test for each // case. t.Run(tc.name, func(t *testing.T) { hd := humanDate(tc.input) if hd != tc.expect { t.Errorf("got %q; expect %q", hd, tc.expected) } }) } }
zzh@ZZHPC:/zdata/Github/snippetbox$ go test -v ./cmd/web === RUN TestHumanDate === RUN TestHumanDate/UTC === RUN TestHumanDate/Empty template_test.go:43: got "01 Jan 0001 at 00:00"; expect "" === RUN TestHumanDate/CET template_test.go:43: got "17 Mar 2024 at 10:15"; expect "17 Mar 2024 at 09:15" --- FAIL: TestHumanDate (0.00s) --- PASS: TestHumanDate/UTC (0.00s) --- FAIL: TestHumanDate/Empty (0.00s) --- FAIL: TestHumanDate/CET (0.00s) FAIL FAIL snippetbox/cmd/web 0.002s FAIL
So here we can see the individual output for each of our sub-tests. As you might have guessed, our first test case passed but the Empty and CET tests both failed. Notice how — for the failed test cases — we get the relevant failure message and filename and line number in the output?
Let’s head back to our humanDate() function and update it to fix these two problems:
func humanDate(t time.Time) string { if t.IsZero() { return "" } // Convert the time to UTC before formatting it. return t.UTC().Format("02 Jan 2006 at 15:04") }
zzh@ZZHPC:/zdata/Github/snippetbox$ go test -v ./cmd/web === 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/cmd/web 0.002s
Helpers for test assertions
As I mentioned briefly earlier, over the next few chapters we’ll be writing a lot of test assertions that are a variation of this pattern:
if actualValue != expectedValue { t.Errorf("got %v; want %v", actualValue, expectedValue) }
Let’s quickly abstract this code into a helper function.
If you’re following along, go ahead and create a new internal/assert package.
Create a new file named assert.go. And then add the following code:
package assert import "testing" func Equal[T comparable](t *testing.T, actual, expected T) { t.Helper() if actual != expected { t.Errorf("got: %v; expect: %v", actual, expected) } }
Note: The t.Helper() function that we’re using in the code above indicates to the Go test runner that our Equal() function is a test helper. This means that when t.Errorf() is called from our Equal() function, the Go test runner will report the filename and line number of the code which called our Equal() function in the output.
With that in place, we can simplify our TestHumanDate() test like so:
package main import ( "snippetbox/internal/assert" "testing" "time" ) func TestHumanDate(t *testing.T) { tests := []struct { name string input time.Time expected string }{ { name: "UTC", input: time.Date(2024, 3, 17, 10, 15, 0, 0, time.UTC), expected: "17 Mar 2024 at 10:15", }, { name: "Empty", input: time.Time{}, expected: "", }, { name: "CET", input: time.Date(2024, 3, 17, 10, 15, 0, 0, time.FixedZone("CET", 1*60*60)), expected: "17 Mar 2024 at 09:15", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { hd := humanDate(tc.input) // Use the new assert.Equal() helper to compare the actual and expected values. assert.Equal(t, hd, tc.expected) }) } }
Sub-tests without a table of test cases
It’s important to point out that you don’t need to use sub-tests in conjunction with table-driven tests (like we have done so far). It’s perfectly valid to execute sub-tests by calling t.Run() consecutively in your test functions, similar to this:
func TestExample(t *testing.T) { t.Run("Example sub-test 1", func(t *testing.T) { // Do a test. }) t.Run("Example sub-test 2", func(t *testing.T) { // Do another test. }) t.Run("Example sub-test 3", func(t *testing.T) { // And another... }) }
Testing HTTP handlers and middleware
Let’s move on and discuss some specific techniques for unit testing your HTTP handlers.
All the handlers that we’ve written for this project so far are a bit complex to test, and to introduce things I’d prefer to start off with something more simple.
So, if you’re following along, head over to your handlers.go file and create a new ping handler function which returns a 200 OK status code and an "OK" response body. It’s the type of handler that you might want to implement for status-checking or uptime monitoring of your server.
func ping(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
Recording responses
Go provides a bunch of useful tools in the net/http/httptest package for helping to test your HTTP handlers.
One of these tools is the httptest.ResponseRecorder type. This is essentially an implementation of http.ResponseWriter which records the response status code, headers and body instead of actually writing them to a HTTP connection.
So an easy way to unit test your handlers is to create a new httptest.ResponseRecorder , pass it to the handler function, and then examine it again after the handler returns.
First, follow the Go conventions and create a new handlers_test.go file to hold the test.
package main import ( "bytes" "io" "net/http" "net/http/httptest" "snippetbox/internal/assert" "testing" ) 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. res := rr.Result() // Check that the status code written by the ping handler was 200. assert.Equal(t, res.StatusCode, http.StatusOK) // And we can check that the response body written by the ping handler equals "OK". defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } body = bytes.TrimSpace(body) assert.Equal(t, string(body), "OK") }
Note: In the code above we use the t.Fatal() function in a couple of places to handle situations where there is an unexpected error in our test code. When called, t.Fatal() will mark the test as failed, log the error, and then completely stop execution of the current test (or sub-test). Typically you should call t.Fatal() in situations where it doesn’t make sense to continue the current test — such as an error during a setup step, or where an unexpected error from a Go standard library function means you can’t proceed with the test.
zzh@ZZHPC:/zdata/Github/snippetbox$ 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/cmd/web 0.002s
Testing middleware
First you’ll need to create a cmd/web/middleware_test.go file to hold the test.
package main import ( "bytes" "io" "net/http" "net/http/httptest" "snippetbox/internal/assert" "testing" ) func TestCommonHeaders(t *testing.T) { 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, chich writes // a 200 status code and "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* // an 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. res := 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, res.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, res.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, res.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, res.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, res.Header.Get("X-XSS-Protection"), expectedValue) // Check that the middleware has correctly set the Server header on the response. expectedValue = "Go" assert.Equal(t, res.Header.Get("Server"), expectedValue) // Check that the middleware has correctly called the next handler in line and the response // status code and body are expected. assert.Equal(t, res.StatusCode, http.StatusOK) defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } body = bytes.TrimSpace(body) assert.Equal(t, string(body), "OK") }
zzh@ZZHPC:/zdata/Github/snippetbox$ 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/cmd/web 0.002s
So, in summary, a quick and easy way to unit test your HTTP handlers and middleware is to simply call them using the httptest.ResponseRecorder type. You can then examine the status code, headers and response body of the recorded response to make sure that they are working as expected.
标签:Web,http,TestHumanDate,10,0.00,PASS,test,time,Go From: https://www.cnblogs.com/zhangzhihui/p/18401846