单测

Hertz 为用户提供的单元测试能力。

一个好的项目的构建离不开单元测试。为了帮助使用者构建出好的项目,hertz 当然也提供了单元测试的工具。

原理和 golang httptest 类似,都是不经过网络只执行 ServeHTTP 返回执行后的 response。

创建请求上下文

func CreateUtRequestContext(method, url string, body *Body, headers ...Header) *app.RequestContext

CreateUtRequestContext

返回一个 app.RequestContext 对象,用于单元测试。

函数签名:

func CreateUtRequestContext(method, url string, body *Body, headers ...Header) *app.RequestContext

示例代码:

import (
	"bytes"
	"testing"

	"github.com/cloudwego/hertz/pkg/common/test/assert"
	"github.com/cloudwego/hertz/pkg/common/ut"
)

func TestCreateUtRequestContext(t *testing.T) {
	body := "1"
	method := "PUT"
	path := "/hey/dy"
	headerKey := "Connection"
	headerValue := "close"
	c := ut.CreateUtRequestContext(method, path, &ut.Body{Body: bytes.NewBufferString(body), Len: len(body)},
		ut.Header{Key: headerKey, Value: headerValue})

	assert.DeepEqual(t, method, string(c.Method()))
	assert.DeepEqual(t, path, string(c.Path()))
	body1, err := c.Body()
	assert.DeepEqual(t, nil, err)
	assert.DeepEqual(t, body, string(body1))
	assert.DeepEqual(t, headerValue, string(c.GetHeader(headerKey)))
}

发送请求

func PerformRequest(engine *route.Engine, method, url string, body *Body, headers ...Header) *ResponseRecorder

PerformRequest

PerformRequest 函数在没有网络传输的情况下向指定 engine 发送构造好的请求。

url 可以是标准的相对路径也可以是绝对路径。

如果想设置流式的请求体,可以通过 server.WithStreamBody(true) 将 engine.streamRequestBody 设置为 true 或者将 body 的 len 设置为 -1。

该函数返回 ResponseRecorder 对象

函数签名:

func PerformRequest(engine *route.Engine, method, url string, body *Body, headers ...Header) *ResponseRecorder

示例代码:

import (
   "bytes"
   "context"
   "testing"

   "github.com/cloudwego/hertz/pkg/app"
   "github.com/cloudwego/hertz/pkg/common/config"
   "github.com/cloudwego/hertz/pkg/common/test/assert"
   "github.com/cloudwego/hertz/pkg/common/ut"
   "github.com/cloudwego/hertz/pkg/route"
)

func TestPerformRequest(t *testing.T) {
   router := route.NewEngine(config.NewOptions([]config.Option{}))
   router.GET("/hey/:user", func(ctx context.Context, c *app.RequestContext) {
      user := c.Param("user")
      assert.DeepEqual(t, "close", c.Request.Header.Get("Connection"))
      c.Response.SetConnectionClose()
      c.JSON(201, map[string]string{"hi": user})
   })

   w := ut.PerformRequest(router, "GET", "/hey/hertz", &ut.Body{bytes.NewBufferString("1"), 1},
      ut.Header{"Connection", "close"})
   resp := w.Result()
   assert.DeepEqual(t, 201, resp.StatusCode())
   assert.DeepEqual(t, "{\"hi\":\"hertz\"}", string(resp.Body()))
}

接收响应

在执行 PerformRequest 函数时,内部已经调用了 NewRecorder, Header, Write, WriteHeader, Flush 等函数,用户只需调用 Result 函数拿到返回的 protocol.Response 对象进行单测即可。

ResponseRecorder 对象

用于记录 handler 的响应信息,内容如下:

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

	// header contains the headers explicitly set by the Handler.
	// It is an internal detail.
	header *protocol.ResponseHeader

	// 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

	result      *protocol.Response // cache of Result's return value
	wroteHeader bool
}

该对象提供的方法如下:

函数签名 说明
func NewRecorder() *ResponseRecorder 返回初始化后的 ResponseRecorder 对象
func (rw *ResponseRecorder) Header() *protocol.ResponseHeader 返回 ResponseRecorder.header
func (rw *ResponseRecorder) Write(buf []byte) (int, error) []byte 类型的数据写入 ResponseRecorder.Body
func (rw *ResponseRecorder) WriteString(str string) (int, error) string 类型的数据写入 ResponseRecorder.Body
func (rw *ResponseRecorder) WriteHeader(code int) 设置 ResponseRecorder.Code 以及 ResponseRecorder.header.SetStatusCode(code)
func (rw *ResponseRecorder) Flush() 实现了 http.Flusher,将 ResponseRecorder.Flushed 设置为 true
func (rw *ResponseRecorder) Result() *protocol.Response 返回 handler 生成的响应信息,至少包含 StatusCode, Header, Body 以及可选的 Trailer,未来将支持返回更多的响应信息

与业务 handler 配合使用

假如已经创建了 handler 以及一个函数 Ping():


package handler

import (
	"context"

	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/common/utils"
)

// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
	c.JSON(200, utils.H{
		"message": "pong",
	})
}

可以在单元测试中直接对 Ping() 函数进行测试:

package handler

import (
	"bytes"
	"testing"

	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/common/test/assert"
	"github.com/cloudwego/hertz/pkg/common/ut"
)

func TestPerformRequest(t *testing.T) {
	h := server.Default()
	h.GET("/ping", Ping)
	w := ut.PerformRequest(h.Engine, "GET", "/ping", &ut.Body{bytes.NewBufferString("1"), 1},
		ut.Header{"Connection", "close"})
	resp := w.Result()
	assert.DeepEqual(t, 201, resp.StatusCode())
	assert.DeepEqual(t, "{\"message\":\"pong\"}", string(resp.Body()))
}

之后对 Ping() 函数进行修改,单元测试文件不需要复制相同的业务逻辑。

更多 examples 参考 pkg/common/ut 中的单测文件。