转载

Golang HTTP Handler测试

当你用Go写了一个Http的web服务之后,也许你想通过单元测试来测试你的handler函数。你虽然已经使用了Go的 net/http 包。也许你不太确定从哪里开始测试,也不知道如何正确的处理程序返回的数据比如:HTTP status codes,HTTP headers或者response bodies。

让我们看看Go语言是如何实现这些的呢,比如:依赖注入和模拟REST。

一个简单的Handler

我们先开始写一个简单的测试示例:我们要确保我们的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)

测试中使用context.Context

如果我们的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)     } 

这个例子有点复杂,但是也告诉了我们:

  • 去掉我们的数据库实现: 我们不需要为单元测试准备测试数据库
  • 创建一个存根,故意抛出错误,这样我们可以测试我们的处handlers正确的状态代码(如HTTP 500)和或写预期response
  • 我们如何创建一个正确的*datastore.Project和测试。

参考文献:

Testing Your (HTTP) Handlers in Go

Golang HTTP Handler测试

原文  https://xiequan.info/golang-http-handler测试/
正文到此结束
Loading...