Go httptest - 在单元测试中进行http模拟测试

简要介绍如何在单元测试过程中,使用httptest包,进行http模拟测试

1 httptest包简要介绍

net/http/httptest包提供了一些HTTP测试有用的工具,通常使用httptest包的流程:模拟一个真实的HTTP Server,设置Handler函数,模拟http请求(利用http包),断言结果

在HTTP Server的模拟上,主要由两类,一类是利用httptest.NewRecorder()方法仅做http.ResponseWriter接口实现,然后模拟http请求的流程,实际上是没有任何真实的HTTP网络监听的;另一类是诸如httptest.NewServer()httptest.NewTLSServer()httptest.NewUnstartedServer()会有真实的网络套接字监听,测试完成后关闭套接字,后面几个使用方式都类似,后面简要介绍下它们的差别。

  1. 实现http.ResponseWriter接口,通过Mock HTTP Response模拟HTTP响应
    1. w := httptest.NewRecorder()
    2. req := httptest.NewRequest()
    3. resp := handler(w, req)
  2. 真实创建HTTP测试服务,创建后就立马监听,通过http.Get(ts.URL)http.Post(ts.URL)等发起真实HTTP请求
    • ts := httptest.NewServer()
    • ts := httptest.NewTLSServer()
  3. 真实创建HTTP测试服务,创建后可以通过ts来说设置相关参数,并需要通过StartTLS()启动HTTP测试服务,再进行HTTP请求测试
    1. ts := httptest.NewUnstartedServer()
    2. ts.EnableHTTP2 = true: 设置参数
    3. ts.StartTLS():启动

另外,注意真实创建HTTP测试服务,需要在UT结束时候关闭打开的资源,包括套接字资源:defer ts.Close()以及defer res.Body.Close()

1.1 通过NewRecorder() 模拟HTTP请求和响应

以下例子中:

  1. 通过req := httptest.NewRequest()模拟HTTP请求
  2. 通过w := httptest.NewRecorder()模拟HTTP响应
  3. 通过handler(w, req)函数模拟HTTP处理过程,
  4. 最后通过w.Result()获取函数处理结果,并以ioutil.ReadAll()函数读取

实际整个过程没有任何网络连接的过程,相当于只是http.ResponseWriter接口的实现,并mock HTTP请求数据返回

func TestRespRecorder(t *testing.T) {
	// define handler
	handler := func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, "<html><body>Hello World!</body></html>")
	}
	// define request
	req := httptest.NewRequest("GET", "http://httpbin.org/ip", nil)
	t.Logf("%+v", req)
	// new an recorder impl http.ResponseWriter
	w := httptest.NewRecorder()
	// handle
	handler(w, req)
	// get fake http response
	resp := w.Result()
	body, _ := ioutil.ReadAll(resp.Body)
	t.Logf("%+v", resp)
	t.Log(string(body))
}

1.2 通过NewUnstartedServer()创建测试服务器、设置参数、显示启动测试HTTP服务

该示例通过ts := httptest.NewUnstartedServer()创建了一个真实的HTTP测试服务,并通过ts.EnabledHTTP2=true设置服务器参数,最后通过ts.StartTLS()启动测试服务;

因为是真实的网络监听,因此需要在UT结束时候,把套接字关闭,因此需要执行defer ts.Closer()

另外,发起请求是通过res, err := ts.Client().Get(ts.URL)实现,ts.Client()就是生成的HTTP Client,ts.URL就是模拟出来的测试URL(比如https://127.0.0.1:56145);

后续http response处理,与正常的http response处理过程无差异,比如资源文件打开的关闭操作:res.Body.Close()

func TestUnstartedHTTP2(t *testing.T) {
	handler := func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, %s", r.Proto)
	}
	// 创建一个server但并不启动他
	ts := httptest.NewUnstartedServer(http.HandlerFunc(handler))
	// 设置
	ts.EnableHTTP2 = true
	// 启动
	ts.StartTLS()
	t.Logf("http server addr: %s", ts.URL)
	defer ts.Close()

	// 请求测试地址
	res, err := ts.Client().Get(ts.URL)
	if err != nil {
		log.Fatal(err)
	}
	greeting, err := ioutil.ReadAll(res.Body)
	if err != nil {
		log.Fatal(err)
	}
	res.Body.Close()
	t.Logf("%s", greeting)
}

1.3 通过NewServer()、NewTLSServer() 创建HTTP测试服务

httptest.NewServer()httptest.NewUnstartedServer()类似,但NewServer()是默认就启动了HTTP服务,不像httptest.NewUnstartedServer()还需要显示的执行StartTLS()方法。

httptest.NewTLSServer()是在httptest.NewServer()基础上,增加了TLS加密传输特性,用于模拟HTTPS请求测试。

另外,默认情况下通过http.HandlerFunc(handler)返回的http Content-Type响应是text/plain,有需要可以通过w.Header().Set("Content-Type", "application/json")进行显示设置HTTP响应头。

1.3.1 NewServer()模拟服务器

func TestNormalHTTPServer(t *testing.T) {
	// 创建一个普通HTTP1.1测试服务
	handler := func(w http.ResponseWriter, r *http.Request) {
		// json header
		w.Header().Set("Content-Type", "application/json")
		// json回包
		m := map[string]interface{}{
			"id":   100,
			"name": "clark",
			"likes": []string{"play game", "watch tv", "read book",},
		}
		jdt, err := json.Marshal(m)
		if err != nil {
			t.Fatal(err)
		}
		w.Write(jdt)
		// fmt.Fprintf(w, "%s", jdt)
	}
	ts := httptest.NewServer(http.HandlerFunc(handler))
	defer ts.Close()

	// 发起http请求
	res, err := http.Get(ts.URL)
	if err != nil {
		log.Fatal(err)
	}
	t.Logf("%s", res.Header)
	t.Logf("%s", res.Request.URL)

    // http响应处理
	greeting, err := ioutil.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		log.Fatal(err)
	}
	t.Logf("%s", greeting)
}

1.3.2 NewTLSServer()模拟TLS服务器

通过res, err := ts.Client().Get(ts.URL)ts.Client模拟了一个HTTP Client,配置了TLS证书信任,并在Server.Close()时候关闭闲置连接。

func TestTLSHTTP(t *testing.T) {
	// 直接创建一个TLS测试Server
	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, client")
	}))
	defer ts.Close()

	// 请求测试地址
	res, err := ts.Client().Get(ts.URL)
	if err != nil {
		log.Fatal(err)
	}
	greeting, err := ioutil.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		log.Fatal(err)
	}

	t.Logf("%s", greeting)
}