当你用Go写了一个Http的web服务之后,也许你想通过单元测试来测试你的handler函数。虽然你已经使用了Go的net/http
包。也许你不太确定从哪里开始测试,也不知道如何正确处理程序返回的数据比如:HTTP status codes,HTTP headers或者response bodies。
让我们看看Go语言是如何实现这些呢,比如:依赖注入和模拟REST。
一个简单的Handler
我们先开始写一个简单的测试示例:我们要确保我们的handler返回一个http 状态码为200的HTTP请求。
handler代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// handlers.go package handlers // e.g. http.HandleFunc("/health-check", HealthCheckHandler) func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { // A very simple health check. w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") // In the future we could report back on the status of our DB, or our cache // (e.g. Redis) by performing a simple PING, and include them in the response. io.WriteString(w, `{"alive": true}`) } |
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// handlers_test.go package handlers import ( "net/http" "testing" ) func TestHealthCheckHandler(t *testing.T) { // Create a request to pass to our handler. We don't have any query parameters for now, so we'll // pass 'nil' as the third parameter. req, err := http.NewRequest("GET", "/health-check", nil) if err != nil { t.Fatal(err) } // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. rr := httptest.NewRecorder() handler := http.HandlerFunc(HealthCheckHandler) // Our handlers satisfy http.Handler, so we can call their ServeHTTP method // directly and pass in our Request and ResponseRecorder. handler.ServeHTTP(rr, req) // Check the status code is what we expect. if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } // Check the response body is what we expect. expected := `{"alive": true}` if rr.Body.String() != expected { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected) } } |
我们可以看到使用Go的testing and httptest 包来测试我们的handler非常的简单。我们构造一个 *http.Request
,和一个*httptest.ResponseRecorder,然后检查我们handler返回的:status code, body
还可以通过httptest.NewServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// handlers_test.go package handlers import ( "fmt" "io/ioutil" "log" "net/http" "net/http/httptest" ) func TestHealthCheckHandler(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client") })) defer ts.Close() res, err := http.Get(ts.URL) if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Printf("%s", greeting) } |
如果我们的handler还需要一些特定的请求参数,或者特定的headers。我们可以通过下面方式测试:
1 2 3 4 5 6 7 8 9 10 11 12 |
// e.g. GET /api/projects?page=1&per_page=100 req, err := http.NewRequest("GET", "/api/projects", // Note: url.Values is a map[string][]string url.Values{"page": {"1"}, "per_page": {"100"}}) if err != nil { t.Fatal(err) } // Our handler might also expect an API key. req.Header.Set("Authorization", "Bearer abc123") // Then: call handler.ServeHTTP(rr, req) like in our first example. |
如果我们想更进一步的测试我们handler特殊的请求或者中间件的话。你可以在内部定义一个匿名函数来捕捉外部声明的变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Declare it outside the anonymous function var token string test http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ // Note: Use the assignment operator '=' and not the initialize-and-assign // ':=' operator so we don't shadow our token variable above. token = GetToken(r) // We'll also set a header on the response as a trivial example of // inspecting headers. w.Header().Set("Content-Type", "application/json") }) // Check the status, body, etc. if token != expectedToken { t.Errorf("token does not match: got %v want %v", token, expectedToken) } if ctype := rr.Header().Get("Content-Type"); ctype != "application/json") { t.Errorf("content type header does not match: got %v want %v", ctype, "application/json") } |
提示:要让字符串像application/json
或者 Content-Type一样的包常量,这样你就不会一遍一遍把它们输错。一个输入错误可能会造成意想不到的结果,因为你测试的东西和你想的东西不是一样的。
你应确保你的测试不仅仅是成功的,而且失败之后:你所测试的hanlder应该返回错误信息比如(HTTP 403, or a HTTP 500)
测试中使用context.Context
如果我们的handler通过context.Context传递数据会怎样?我们如何创建一个上下文呢?比如身份验证的token或者我们用User类型。
假设:你有提供自定义处理程序类型ServeHTTPC(context.Context, http.ResponseWriter, *http.Request)
. Go 1.7将会添加context.Context to http.Request。
注意接下来的示例:我使用了Goji mux/router作为context.Context作为兼容处理方式。这种方法使用context.Context
.可以用于任何路由/多路复用器/框架。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
func TestGetProjectsHandler(t *testing.T) { req, err := http.NewRequest("GET", "/api/users", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() // e.g. func GetUsersHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) goji.HandlerFunc(GetUsersHandler) // Create a new context.Context and populate it with data. ctx = context.Background() ctx = context.WithValue(ctx, "app.auth.token", "abc123") ctx = context.WithValue(ctx, "app.user", &YourUser{ID: "qejqjq", Email: "user@example.com"}) // Pass in our context, *http.Request and ResponseRecorder. handler.ServeHTTPC(ctx, rr, req) // Check the status code is what we expect. if status := rr.Code; status != http.StatusOK { t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) } // We could also test that our handler correctly mutates our context.Context: // this is useful if our handler is a piece of middleware. if id , ok := ctx.Value("app.req.id").(string); !ok { t.Errorf("handler did not populate the request ID: got %v", id) } } |
模拟数据库调用
接下来的代码我们handlers通过datastore.ProjectStore(一个接口类型)的三种方法(Create, Get, Delete)来模拟测试handlers返回正确的状态码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
// handlers_test.go package handlers // Throws errors on all of its methods. type badProjectStore struct { // This would be a concrete type that satisfies datastore.ProjectStore. // We embed it here so that our goodProjectStub type has all the methods // needed to satisfy datastore.ProjectStore, without having to stub out // every method (we might not want to test all of them, or some might be // not need to be stubbed. *datastore.Project } func (ps *projectStoreStub) CreateProject(project *datastore.Project) error { return datastore.NetworkError{errors.New("Bad connection"} } func (ps *projectStoreStub) GetProject(id string) (*datastore.Project, error) { return nil, datastore.NetworkError{errors.New("Bad connection"} } func TestGetProjectsHandlerError(t *testing.T) { var store datastore.ProjectStore = &badProjectStore{} // We inject our environment into our handlers. // Ref: http://elithrar.github.io/article/http-handler-error-handling-revisited/ env := handlers.Env{Store: store, Key: "abc"} req, err := http.NewRequest("GET", "/api/projects", nil) if err != nil { t.Fatal(err) } rr := httptest.Recorder(){} // Handler is a custom handler type that accepts an env and a http.Handler // GetProjectsHandler here calls GetProject, and should raise a HTTP 500 if // it fails. Handler{env, GetProjectsHandler) handler.ServeHTTP(rr, req) // We're now checking that our handler throws an error (a HTTP 500) when it // should. if status := rr.Code; status != http.StatusInternalServeError { t.Errorf("handler returned wrong status code: got %v want %v" rr.Code, http.StatusOK) } // We'll also check that it returns a JSON body with the expected error. expected := []byte(`{"status": 500, "error": "Bad connection"}`) if !bytes.Equals(rr.Body.Bytes(), expected) { t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.Bytes(), expected) } |
这个例子有点复杂,但是也告诉了我们:
- 去掉我们的数据库实现: 我们不需要为单元测试准备测试数据库
- 创建一个存根,故意抛出错误,这样我们可以测试我们的handlers正确的状态代码(如HTTP 500)和或写预期response
- 我们如何创建一个正确的*datastore.Project和测试。
参考文献:
Testing Your (HTTP) Handlers in Go
有疑问加站长微信联系(非本文作者)