0%

Go 单元测试完全指南(五)- http 测试

很多时候我们会使用 golang 来作为 web 服务的后端,这时候我们就需要对我们的 http 接口进行测试。Go 语言的标准库提供了一个非常方便的测试工具:net/http/httptest 包,可以用来测试 http 服务。 比如对自己写的 http 接口进行测试,通过 mock 掉 ResponseWriter;又或者在单元测试中 mock 掉对外部的 http 请求,让我们的测试不依赖于外部的 http 服务。

http 测试的作用

  1. 测试你写的 http 接口:具体来说,就是测试你的 http handler 是否按照预期工作。比如你的 handler 是否正确的处理了请求参数,是否正确的返回了响应。
  2. mock 掉对外部的 http 请求:在单元测试中,我们通常不希望依赖于外部的 http 服务,因为外部的服务可能会有变化,这样会导致我们的测试不稳定。

如何测试你的 http 接口

假设我们有一个返回 Hello, world! 的 http 接口:

1
2
3
4
5
6
7
package main

import "net/http"

func HelloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}

我们单元测试中,可能会想知道,自己构建一个 http.Request 得到的响应是否是我们预期的。比如我们想知道,是否返回了 Hello, World!,状态码是否是 200 等。

这个时候,我们就可以使用 httptest.NewRecorder 来模拟一个 http.ResponseWriter,它跟 http.ResponseWriter 的行为一样, 但是它不会真的发送响应到客户端,而是把响应保存在内存中,这样我们就可以方便的测试我们的 handler 是否按照预期工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func TestHelloHandler(t *testing.T) {
// 创建一个请求
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
// 创建一个响应
w := httptest.NewRecorder()
// 调用我们的处理函数
HelloHandler(w, req)

// 检查响应码
if w.Code != http.StatusOK {
t.Errorf("响应码错误: %d", w.Code)
}
// 检查响应体
if w.Body.String() != "Hello, World!" {
t.Errorf("响应体错误: %q", w.Body.String())
}
}

说明:

  • 我们使用 httptest.NewRequest 来创建一个请求,这个请求可以用来模拟一个 http 请求。
  • 我们使用 httptest.NewRecorder 来创建一个响应,这个响应可以用来模拟一个 http.ResponseWriter
  • 调用 HelloHandler 的过程实际上等同于我们的 http 服务接收到了一个请求,然后返回了一个响应。
  • 最后我们检查了响应码和响应体是否符合预期。

httptest.NewRecorder 返回了一个 ResponseRecorder 实例,它有如下字段:

  • Code 记录了响应的状态码
  • Body 记录了响应的 body
  • HeaderMap 记录了响应的 header
  • Flushed 记录了响应是否已经被发送

它具有如下方法:

  • Header() 返回响应的 header
  • Result() 返回响应的 http.Response 实例
  • Write([]byte) 写入响应的 body(它的调用发生在我们的 http handler 中)
  • WriteString(string) 写入响应的 body(跟 Write([]byte) 类似,只是参数类型不一样)
  • WriteHeader(int) 写入响应的状态码
  • Flush() 将响应发送到客户端

总的来说,我们可以通过 httptest.NewRecorder 来模拟一个 http.ResponseWriter,然后调用我们的 handler,最后检查响应是否符合预期。 也就是说,我们不用启动一个真正的 http 客户端来对我们的接口进行测试;当然,我们也不需要真的启动一个真正的 http 服务。

mock 掉对外部的 http 请求

有时候,我们开发的功能会依赖于外部的 http 服务,比如调用一个第三方的接口。在单元测试中,我们通常不希望依赖于外部的服务,因为外部的服务可能会有变化,这样会导致我们的测试不稳定。

如何 mock ?

  1. 定义一个 http.Handler 的实现,然后在这个 handler 中返回我们预期的响应
  2. 使用 httptest.NewServer 来启动一个 mock http 服务,使用上面的 handler
  3. 使用 httptest.Server 实例的 URL 字段来获取这个 mock 服务的地址
  4. 最终,当我们访问这个 mock 服务的时候,它会返回我们预期的响应

示例

先定义一个 http.Handler,我们判断请求的路径是 /hello 的时候,返回 Hello, World!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "net/http"

type MyHandler struct{}

func (m *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/hello":
w.Write([]byte("Hello, World!"))
default:
http.Error(w, "Not Found", http.StatusNotFound)
}
}

接着,启动一个 httptest.Server,并指定我们的 HandlerFunc

1
2
3
4
server := httptest.NewServer(&MyHandler{})
defer server.Close()

println(server.URL)

这里获取到的 server.URL 就是我们的 mock 服务的地址,我们可以通过这个地址来访问我们的 mock 服务。

我们可以将自己代码中访问外部服务的地址替换为 server.URL,这样我们的测试就不会依赖于外部服务,而是依赖于我们自己的 mock 服务。这样就可以实现对外部服务的 mock。

下面是一个完整的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// myFunc 请求 url 并返回响应
func myFunc(url string) string {
resp, err := http.Get(url)
if err != nil {
return err.Error()
}
defer resp.Body.Close()

res, _ := io.ReadAll(resp.Body)
return string(res)
}

func TestHello(t *testing.T) {
server := httptest.NewServer(&MyHandler{})
defer server.Close()

res := myFunc(server.URL + "/hello")
if res != "Hello, World!" {
t.Errorf("Expected 'Hello, World!', got %s", res)
}
}

上面这个例子中:

  1. 我们定义了一个 myFunc 函数,它会请求一个 url 并返回响应
  2. 我们定义了一个 TestHello 测试函数,它会启动一个 mock 服务,然后调用 myFunc 函数,最后检查响应是否符合预期

它实现的功能是:我们的 myFunc 函数会请求 mock 服务的 /hello 路径,然后返回响应。 在实际开发中,myFunc 接收的 url 可能是一个外部服务的地址,比如第三方 api 的地址。

有了 httptest,现在我们只需要替换 myFunc 中的 url 为 server.URL,就可以实现对外部服务的 mock。

总结

在本文中,我们深入探讨了如何使用 Go 语言的 net/http/httptest 包来测试 HTTP 服务。我们首先介绍了 HTTP 测试的作用,包括测试自定义的 HTTP 接口以及模拟外部 HTTP 请求,以确保单元测试的稳定性和可控性。

接着,我们详细介绍了如何测试自定义的 HTTP 接口,通过使用 httptest.NewRecorder 来模拟 http.ResponseWriter,从而捕获 HTTP 处理函数的输出。我们还展示了如何检查响应码和响应体,以验证处理函数的行为是否符合预期。

然后,我们讨论了如何模拟外部 HTTP 请求,通过定义一个 http.Handler 并使用 httptest.NewServer 来创建一个 mock HTTP 服务器。我们看到,通过这种方式,我们可以控制外部服务的响应,从而在测试环境中隔离外部依赖。