golang web开发 Handler测试利器httptest


原文链接: 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

  1. httptest.NewRequest 用于模拟一个Request请求
  2. 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!")
}

参考

  1. https://golang.org/src/net/http/httptest/example_test.go
  2. https://golang.org/pkg/net/http/httptrace/
  3. 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

`