当你用Go写了一个Http的web服务之后,也许你想通过单元测试来测试你的handler函数。你虽然已经使用了Go的 net/http
包。也许你不太确定从哪里开始测试,也不知道如何正确的处理程序返回的数据比如:HTTP status codes,HTTP headers或者response bodies。
让我们看看Go语言是如何实现这些的呢,比如:依赖注入和模拟REST。
我们先开始写一个简单的测试示例:我们要确保我们的handler返回一个http 状态码为200的HTTP请求。
handler代码:
// handlers.go package handlers // e.g. http.HandleFunc("/health-check", HealthCheckHandler) funcHealthCheckHandler(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}`) }
测试代码:
// handlers_test.go package handlers import ( "net/http" "testing" ) funcTestHealthCheckHandler(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
如果我们的handler还需要一些特定的请求参数,或者特定的headers。我们可以通过下面方式测试:
// 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特殊的请求或者中间件的的话。你可以定义在内部定义一个匿名函数来捕捉外部申明的变量:
// Declare it outside the anonymous function var tokenstring testhttp.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)
如果我们的handler通过 context.Context传递数据会怎样?我们如何创建一个上下文背景呢?比如身份验证的token或者我们的用User的类型。
假设: 你 有 提供 自定义 处理程序 类型 ServeHTTPC(context.Context, http.ResponseWriter, *http.Request)
. Go 1.7将会添加 add context.Context to http.Request 。
注意接下来的示例:我使用了 Goji mux/router作为context.Context作为兼容处理方式。这种方法使用 context.Context
.可以用于任何路由/多路复用器/框架。
funcTestGetProjectsHandler(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返回正确的状态码。
// 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(idstring) (*datastore.Project, error) { return nil, datastore.NetworkError{errors.New("Bad connection"} } funcTestGetProjectsHandlerError(t *testing.T) { var storedatastore.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) }
这个例子有点复杂,但是也告诉了我们:
参考文献:
Testing Your (HTTP) Handlers in Go
Golang HTTP Handler测试