Constants

DefaultRemoteAddr是默认的远端地址。如果ResponseRecorder未显式的设置该属性,RemoteAddr方法就会返回该值。

1
const DefaultRemoteAddr = "1.2.3.4"

func NewRequest1.7

1
func NewRequest(method, target string, body io.Reader) *http.Request

NewRequest 返回一个新的服务器访问请求,这个请求可以传递给 http.Handler 以便进行测试。

target 参数的值为 RFC 7230 中提到的“请求目标”(request-target): 它可以是一个路径或者一个绝对 URL。如果 target 是一个绝对 URL,那么 URL 中的主机名(host name)将被使用;否则主机名将为 example.com。

当 target 的模式为 https 时,TLS 字段的值将被设置为一个非 nil 的随意值(dummy value)。

Request.Proto 总是为 HTTP/1.1。

如果 method 参数的值为空, 那么使用 GET 方法作为默认值。

body 参数的值可以为 nil;另一方面,如果 body 参数的值为 *bytes.Reader 类型、 *strings.Reader 类型或者 *bytes.Buffer 类型,那么 Request.ContentLength 将被设置。

为了使用的方便,NewRequest 将在 panic 可以被接受的情况下,使用 panic 代替错误。

如果你想要生成的不是服务器访问请求,而是一个客户端 HTTP 请求,那么请使用 net/http 包中的 NewRequest 函数。

type ResponseRecorder

ResponseRecorder实现了http.ResponseWriter接口,它记录了其修改,用于之后的检查。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type ResponseRecorder struct {
    // Code is the HTTP response code set by WriteHeader.
    //
    // Note that if a Handler never calls WriteHeader or Write,
    // this might end up being 0, rather than the implicit
    // http.StatusOK. To get the implicit value, use the Result
    // method.
    Code int

    // HeaderMap contains the headers explicitly set by the Handler.
    // It is an internal detail.
    //
    // Deprecated: HeaderMap exists for historical compatibility
    // and should not be used. To access the headers returned by a handler,
    // use the Response.Header map as returned by the Result method.
    HeaderMap http.Header

    // Body is the buffer to which the Handler's Write calls are sent.
    // If nil, the Writes are silently discarded.
    Body *bytes.Buffer

    // Flushed is whether the Handler called Flush.
    Flushed bool
    // contains filtered or unexported fields
}

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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!</body></html>")
	}

	req := httptest.NewRequest("GET", "http://example.com/foo", nil)
	w := httptest.NewRecorder()
	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))

}

func NewRecorder

1
func NewRecorder() *ResponseRecorde

NewRecorder返回一个初始化了的ResponseRecorder.

func (*ResponseRecorder) Flush

1
func (rw *ResponseRecorder) Flush()

Flush实现http.Flusher。要测试是否调用了Flush,请参阅rw.Flushed。

func (*ResponseRecorder) Header

1
func (rw *ResponseRecorder) Header() http.Header

Header实现http.ResponseWriter。它返回响应标头以在处理程序中进行更改。若要测试在处理程序完成后编写的标头,请使用Result方法并查看返回的Response值的Header。

func (*ResponseRecorder) Result 1.7

1
func (rw *ResponseRecorder) Result() *http.Response

Result 返回处理器生成的响应。

处理器返回的响应至少会对状态码(StatusCode)、首部(Header)、主体(Body)以及可选的 Trailer 进行设置。 因为未来可能会有更多字段被设置,所以用户不应该在测试里面对结果调用 DeepEqual。

Response.Header 是写入操作第一次调用时的首部快照(snapshot of the headers); 另一方面, 如果处理器没有执行过写入操作, 那么 Response.Header 就是 Result 方法调用时的首部快照。

Response.Body 将被生成为一个非 nil 值,而 Body.Read 则保证不会返回除 io.EOF 之外的其他任何错误。

Result 必须在处理器执行完毕之后调用。

func (*ResponseRecorder) Write

1
func (rw *ResponseRecorder) Write(buf []byte) (int, error)

Write 实现 http.ResponseWriter.如果buf中的数据不为nil,则将其写入rw.Body。

func (*ResponseRecorder) WriteHeader

1
func (rw *ResponseRecorder) WriteHeader(code int)

WriteHeader 实现 http.ResponseWriter.

func (*ResponseRecorder) WriteString 1.6

1
func (rw *ResponseRecorder) WriteString(str string) (int, error)

WriteString 实现 io.StringWriter. 如果buf中的数据不为nil,则将其写入rw.Body。

type Server

Server是一个HTTP服务端,在本地环回接口的某个系统选择的端口监听,用于点对点HTTP测试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type Server struct {
    URL      string // base URL of form http://ipaddr:port with no trailing slash
    Listener net.Listener

    // TLS is the optional TLS configuration, populated with a new config
    // after TLS is started. If set on an unstarted server before StartTLS
    // is called, existing fields are copied into the new config.
    TLS *tls.Config

    // Config may be changed after calling NewUnstartedServer and
    // before Start or StartTLS.
    Config *http.Server
    // contains filtered or unexported fields
}

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
)

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)
}

func NewServer

1
func NewServer(handler http.Handler) *Server

NewServer返回一个新的、已启动的Server。调用者必须在用完时调用Close方法关闭它。

func NewTLSServer

1
func NewTLSServer(handler http.Handler) *Server

NewTLSServer返回一个新的、使用TLS的、已启动的Server。调用者必须在用完时调用Close方法关闭它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
)

func main() {
	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, client")
	}))
	defer ts.Close()

	client := ts.Client()
	res, err := 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)
	}

	fmt.Printf("%s", greeting)
}

func NewUnstartedServer

1
func NewUnstartedServer(handler http.Handler) *Server

NewUnstartedServer返回一个新的、未启动的Server。在修改其配置后,调用者应该调用Start或StartTLS启动它;调在用完时用者必须调用Close方法关闭它。

func (*Server) Certificate 1.9

1
func (s *Server) Certificate() *x509.Certificate

Certificate返回服务器使用的证书,如果服务器不使用TLS,则返回null。

func (*Server) Client1.9

1
func (s *Server) Client() *http.Client

客户端返回配置用于向服务器发出请求的HTTP客户端。它被配置为信任服务器的TLS测试证书,并将在Server.Close上关闭其空闲连接。

func (*Server) Close

1
func (s *Server) Close()

Close关闭服务端,并阻塞直到所有该服务端未完成的请求都结束为止。

func (*Server) CloseClientConnections

1
func (s *Server) CloseClientConnections()

CloseClientConnections关闭当前任何与该服务端建立的HTTP连接。

func (*Server) Start

1
func (s *Server) Start()

Start启动NewUnstartedServer返回的服务端。

func (*Server) StartTLS

1
func (s *Server) StartTLS()

StartTLS启动NewUnstartedServer函数返回的服务端的TLS监听。

模拟server

假设现在有这么一个场景,我们现在有一个功能需要调用免费天气API来获取天气信息,但是这几天该API升级改造暂时不提供联调服务,而Boss希望该服务恢复后我们的新功能能直接上线,我们要怎么在服务不可用的时候完成相关的测试呢?答案就是使用Mock。

net/http/httptest就是原生库里面提供Mock服务的包,使用它不用真正的启动一个http server(亦或者请求任意的server),而且创建方法非常简单。下面我们一起来看看怎么使用它吧。

定义被测接口

将下面的内容保存到weather.go中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package weather

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
)

const (
    ADDRESS = "shenzhen"
)

type Weather struct {
    City    string `json:"city"`
    Date    string `json:"date"`
    TemP    string `json:"temP"`
    Weather string `json:"weather"`
}

func GetWeatherInfo(api string) ([]Weather, error) {
    url := fmt.Sprintf("%s/weather?city=%s", api, ADDRESS)
    resp, err := http.Get(url)

    if err != nil {
        return []Weather{}, err
    }

    if resp.StatusCode != http.StatusOK {
        return []Weather{}, fmt.Errorf("Resp is didn't 200 OK:%s", resp.Status)
    }
    bodybytes, _ := ioutil.ReadAll(resp.Body)
    personList := make([]Weather, 0)

    err = json.Unmarshal(bodybytes, &personList)

    if err != nil {
        fmt.Errorf("Decode data fail")
        return []Weather{}, fmt.Errorf("Decode data fail")
    }
    return personList, nil
}

根据我们前面的场景设定,GetWeatherInfo依赖接口是不可用的,所以resp, err := http.Get(url)这一行的err肯定不为nil。为了不影响天气服务恢复后我们的功能能直接上线,我们在不动源码,从单元测试用例入手来完成测试。

测试代码

将下面的内容保存到weather_test.go中::

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package weather

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
)

var weatherResp = []Weather{
    {
        City:    "shenzhen",
        Date:    "10-22",
        TemP:    "15℃~21℃",
        Weather: "rain",
    },
    {
        City:    "guangzhou",
        Date:    "10-22",
        TemP:    "15℃~21℃",
        Weather: "sunny",
    },
    {
        City:    "beijing",
        Date:    "10-22",
        TemP:    "1℃~11℃",
        Weather: "snow",
    },
}
var weatherRespBytes, _ = json.Marshal(weatherResp)

func TestGetInfoUnauthorized(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusUnauthorized)
        w.Write(weatherRespBytes)
        if r.Method != "GET" {
            t.Errorf("Except 'Get' got '%s'", r.Method)
        }

        if r.URL.EscapedPath() != "/weather" {
            t.Errorf("Except to path '/person',got '%s'", r.URL.EscapedPath())
        }
        r.ParseForm()
        topic := r.Form.Get("city")
        if topic != "shenzhen" {
            t.Errorf("Except rquest to have 'city=shenzhen',got '%s'", topic)
        }
    }))
    defer ts.Close()
    api := ts.URL
    fmt.Printf("Url:%s\n", api)
    resp, err := GetWeatherInfo(api)
    if err != nil {
        t.Errorf("ERR:", err)
    } else {
        fmt.Println("resp:", resp)
    }
}

func TestGetInfoOK(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write(weatherRespBytes)
        if r.Method != "GET" {
            t.Errorf("Except 'Get' got '%s'", r.Method)
        }

        if r.URL.EscapedPath() != "/weather" {
            t.Errorf("Except to path '/person',got '%s'", r.URL.EscapedPath())
        }
        r.ParseForm()
        topic := r.Form.Get("city")
        if topic != "shenzhen" {
            t.Errorf("Except rquest to have 'city=shenzhen',got '%s'", topic)
        }
    }))
    defer ts.Close()
    api := ts.URL
    fmt.Printf("Url:%s\n", api)
    resp, err := GetWeatherInfo(api)
    if err != nil {
        fmt.Println("ERR:", err)
    } else {
        fmt.Println("resp:", resp)
    }
}

简单解释一下上面的部分代码:

  • 我们通过httptest.NewServer创建了一个测试的http server
  • 通过变量r *http.Request读请求设置,通过w http.ResponseWriter设置返回值
  • 通过ts.URL来获取请求的URL(一般都是http://ip:port)也就是实际的请求url
  • 通过r.Method来获取请求的方法,来测试判断我们的请求方法是否正确
  • 获取请求路径:r.URL.EscapedPath(),本例中的请求路径就是"/weather"
  • 获取请求参数:r.ParseForm,r.Form.Get(“city”)
  • 设置返回的状态码:w.WriteHeader(http.StatusOK)
  • 设置返回的内容(也就是我们想要的结果):w.Write(personResponseBytes),注意w.Write()接收的参数是[]byte,所以通过json.Marshal(personResponse)转换。

当然,我们也可以设置其他参数的值,也就是我们在最前面介绍的http.Request/http.ResponseWriter这两个结构体的内容。

测试执行

在终端中进入我们保存上面两个文件的目录,执行go test -v就可以看到下面的测试结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
bingo@Mac httptest$ go test -v
=== RUN   TestGetInfoUnauthorized
Url:http://127.0.0.1:55816
--- FAIL: TestGetInfoUnauthorized (0.00s)
        person_test.go:55: ERR:%!(EXTRA *errors.errorString=Resp is didn't 200 OK:401 Unauthorized)
=== RUN   TestGetInfoOK
Url:http://127.0.0.1:55818
resp: [{shenzhen 10-22 15℃~21℃ rain} {guangzhou 10-22 15℃~21℃ sunny} {beijing 10-22 1℃~11℃ snow}]
--- PASS: TestGetInfoOK (0.00s)
FAIL
exit status 1
FAIL    bingo.com/blogs/httptest        0.016s

可以看到两条测试用例成功了一条失败了一条,失败的原因就是我们设置的接口响应码为401(w.WriteHeader(http.StatusUnauthorized)),这个可能会在调用其他服务时遇到,所以有必要进行测试。更多的响应码我们可以在我们的golang安装目录下找到:

/usr/local/go/src/net/http/status.go

这个文件中定义了几乎所有的http响应码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    StatusContinue           = 100 // RFC 7231, 6.2.1
    StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
    StatusProcessing         = 102 // RFC 2518, 10.1

    StatusOK                   = 200 // RFC 7231, 6.3.1
    StatusCreated              = 201 // RFC 7231, 6.3.2
    StatusAccepted             = 202 // RFC 7231, 6.3.3
    StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
    StatusNoContent            = 204 // RFC 7231, 6.3.5
    StatusResetContent         = 205 // RFC 7231, 6.3.6
    ...

综上,我们可以通过不发送httptest来模拟出httpserver和返回值来进行自己代码的测试.

模拟client

我们用go开发一个Web Server后,打算单元测试写的handler函数,在不知道httptest之前,使用比较笨的方法

就是编译运行该Web Server后,再用go编写一个客户端程序向该Web Server对应的route发送数据然后解析

返回的数据。这个方法测试时非常麻烦,使用httptest来测试的话就非常简单,可以和testing测试一起使用。

被测handler

假设在server中handler已经写好

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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}`)
}

测试代码

测试代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func TestHealthCheckHandler(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)
    }
}

注意:

  • httptest.NewRequest。而不是http.NewRequest
  • httptest.NewRequest的第三个参数可以用来传递body数据,必须实现io.Reader接口。
  • httptest.NewRequest不会返回error,无需进行err!=nil检查。
  • 解析响应时调用了Result函数。

参考: https://www.cnblogs.com/Detector/p/9769840.html https://www.jianshu.com/p/21571fe59ec4