golang web开发 Handler测试利器httptest
https://github.com/julienschmidt/go-http-routing-benchmark
我们用go开发一个Web Server后,打算单元测试写的handler函数,在不知道httptest之前,使用比较笨的方法
就是编译运行该Web Server后,再用go编写一个客户端程序向该Web Server对应的route发送数据然后解析
返回的数据。这个方法测试时非常麻烦,使用httptest来测试的话就非常简单,可以和testing测试一起使用。
Go Http包的处理函数如下,一个用于输入(Request),一个用于输出(Response),很多第三方的web框架都提供了自己的处理函数定义,但都很方便地适配HandlerFunc
type HandlerFunc func(ResponseWriter, *Request)
为了调试,http包提供了ResponseWriter/Request的模拟
httptest
- httptest.NewRequest 用于模拟一个Request请求
- httptest.NewRecorder用于模拟一个Response
package main
import (
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
)
func main() {
handler := func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "<html><body>Hello World![" + r.URL.String() + "]</body></html>")
}
req := httptest.NewRequest("GET", "http://example.com/foo", nil) //httptest.NewRequest 用于模拟一个Request请求
w := httptest.NewRecorder()//httptest.NewRecorder用于模拟一个Response
handler(w, req)
resp := w.Result()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(resp.StatusCode)
fmt.Println(resp.Header.Get("Content-Type"))
fmt.Println(string(body))
// Output:
// 200
// text/html; charset=utf-8
// <html><body>Hello World![http://example.com/foo]</body></html>
}
Server
处理模拟请求和响应,Httptest同样提供了服务器的模拟,不过非常简单,没有路由,只能提供单个响应函数(*在响应函数自行处理另当别论*)
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"log"
)
func main() {
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)
// Output: Hello, client
}
server提供了几个接口,用法有一些简单的区别
// NewServer starts and returns a new Server.
// The caller should call Close when finished, to shut it down.
func NewServer(handler http.Handler) *Server
// NewUnstartedServer returns a new Server but doesn't start it.
//
// After changing its configuration, the caller should call Start or
// StartTLS.
//
// The caller should call Close when finished, to shut it down.
func NewUnstartedServer(handler http.Handler) *Server
// NewTLSServer starts and returns a new Server using TLS.
// The caller should call Close when finished, to shut it down.
func NewTLSServer(handler http.Handler) *Server
启动服务器,并不会阻塞当前gorounte,在内部会开启一个新的gorounte来执行监听任务
func (s *Server) goServe() {
s.wg.Add(1)
go func() {
defer s.wg.Done()
s.Config.Serve(s.Listener)
}()
}
httptrace
此外,http还包含httptrace,用于监听http请求的各种事件,核心就是一个ClientTrace对象,具体的函数,可以godoc
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"os"
)
func main() {
server := httptest.NewServer(http.HandlerFunc(http.NotFound))
defer server.Close()
c := http.Client{}
req, err := http.NewRequest("GET", server.URL, nil)
if err != nil {
panic(err)
}
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Println("Got Conn")
},
ConnectStart: func(network, addr string) {
fmt.Println("Dial start")
},
ConnectDone: func(network, addr string, err error) {
fmt.Println("Dial done")
},
GotFirstResponseByte: func() {
fmt.Println("First response byte!")
},
WroteHeaders: func() {
fmt.Println("Wrote headers")
},
WroteRequest: func(wr httptrace.WroteRequestInfo) {
fmt.Println("Wrote request", wr)
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
fmt.Println("Starting request!")
resp, err := c.Do(req)
if err != nil {
panic(err)
}
io.Copy(os.Stdout, resp.Body)
fmt.Println("Done!")
}
参考
- https://golang.org/src/net/http/httptest/example_test.go
- https://golang.org/pkg/net/http/httptrace/
- http://www.tuicool.com/articles/nQzqmuZ
httptest基本使用方法
假设在server中handler已经写好
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}`)
}
测试如下:
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthCheckHandler(t *testing.T) {
//创建一个请求
req, err := http.NewRequest("GET", "/health-check", nil)
if err != nil {
t.Fatal(err)
}
// 我们创建一个 ResponseRecorder (which satisfies http.ResponseWriter)来记录响应
rr := httptest.NewRecorder()
//直接使用HealthCheckHandler,传入参数rr,req
HealthCheckHandler(rr, req)
// 检测返回的状态码
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
// 检测返回的数据
expected := `{"alive": true}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
然后就可以使用run test来执行测试.
如果Web Server有操作数据库的行为,需要在init函数中进行数据库的连接。
参考官方文档中的样例编写的另外一个测试代码:
func TestHealthCheckHandler2(t *testing.T) {
reqData := struct {
Info string `json:"info"`
}{Info: "P123451"}
reqBody, _ := json.Marshal(reqData)
fmt.Println("input:", string(reqBody))
req := httptest.NewRequest(
http.MethodPost,
"/health-check",
bytes.NewReader(reqBody),
)
req.Header.Set("userid", "wdt")
req.Header.Set("commpay", "brk")
rr := httptest.NewRecorder()
HealthCheckHandler(rr, req)
result := rr.Result()
body, _ := ioutil.ReadAll(result.Body)
fmt.Println(string(body))
if result.StatusCode != http.StatusOK {
t.Errorf("expected status 200,",result.StatusCode)
}
}
注意不同的地方
http.NewRequest 替换为 httptest.NewRequest。
httptest.NewRequest 的第三个参数可以用来传递body数据,必须实现io.Reader接口。
httptest.NewRequest 不会返回error,无需进行 err!=nil检查。
解析响应时没直接使用 ResponseRecorder,而是调用了 Result 函数。
结合context使用
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)
handler := http.HandlerFunc(GetUsersHandler)
// Populate the request's context with our test data.
ctx := req.Context()
ctx = context.WithValue(ctx, "app.auth.token", "abc123")
ctx = context.WithValue(ctx, "app.user",
&YourUser{ID: "qejqjq", Email: "user@example.com"})
// Add our context to the request: note that WithContext returns a copy of
// the request, which we must assign.
req = req.WithContext(ctx)
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)
}
}
参考
httptest doc
Testing Your (HTTP) Handlers in Go
作者:kingeasternsun
链接:https://www.jianshu.com/p/21571fe59ec4