Hertz 学习笔记(14)

329 阅读3分钟

使用 hertz 提供的没有网络传输的接口编写单元测试的示例

首先看代码:

/*
 * Copyright 2022 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package main

import (
	"github.com/cloudwego/hertz/pkg/app/server"
)

func main() {
	h := server.Default(server.WithHostPorts("127.0.0.1:8080"))

	h.Spin()
}

然后看单测代码:

/*
 * Copyright 2022 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package main

import (
	"bytes"
	"context"
	"testing"

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

func TestPerformRequest(t *testing.T) {
	opt := config.NewOptions([]config.Option{})
	router := route.NewEngine(opt)
	// context uri
	router.PUT("/hey/:version", func(ctx context.Context, c *app.RequestContext) {
		version := c.Param("version")
		if string(c.Request.Body()) == "1" {
			assert.Equal(t, "close", c.Request.Header.Get("Connection"))
			c.Response.SetConnectionClose()
			c.JSON(201, map[string]string{"hi": version})
		} else if string(c.Request.Body()) == "" {
			c.AbortWithMsg("unauthorized", 401)
		} else {
			assert.Equal(t, "PUT /hey/dy HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nTransfer-Encoding: chunked\r\n\r\n", string(c.Request.Header.Header()))
			c.String(202, "body:%v", string(c.Request.Body()))
		}
	})

	router.GET("/hey/header", func(ctx context.Context, c *app.RequestContext) {
		assert.Equal(t, "application/json", string(c.GetHeader("Content-Type")))
		assert.Equal(t, 1, c.Request.Header.ContentLength())
		assert.Equal(t, "a", c.Request.Header.Get("dummy"))
	})

	type testReq struct {
		Version string `json:"version"`
	}
	router.POST("/hey/json", func(ctx context.Context, c *app.RequestContext) {
		assert.Equal(t, "application/json", string(c.GetHeader("Content-Type")))
		var req testReq
		if err := c.BindAndValidate(&req); err != nil {
			panic(err)
		}
		assert.Equal(t, "v0.0.1", req.Version)
	})

	w := ut.PerformRequest(router, "PUT", "/hey/version1", &ut.Body{Body: bytes.NewBufferString("1"), Len: 1},
		ut.Header{Key: "Connection", Value: "close"})
	resp := w.Result()
	assert.Equal(t, 201, resp.StatusCode())
	assert.Equal(t, "{\"hi\":\"version1\"}", string(resp.Body()))
	assert.Equal(t, "application/json; charset=utf-8", string(resp.Header.ContentType()))
	assert.Equal(t, true, resp.Header.ConnectionClose())

	// unauthorized user
	w = ut.PerformRequest(router, "PUT", "/hey/version2", nil)
	_ = w.Result()
	resp = w.Result()
	assert.Equal(t, 401, resp.StatusCode())
	assert.Equal(t, "unauthorized", string(resp.Body()))
	assert.Equal(t, "text/plain; charset=utf-8", string(resp.Header.ContentType()))
	assert.Equal(t, 12, resp.Header.ContentLength())

	// special header
	ut.PerformRequest(router, "GET", "/hey/header", nil,
		ut.Header{Key: "content-type", Value: "application/json"},
		ut.Header{Key: "content-length", Value: "1"},
		ut.Header{Key: "dummy", Value: "a"},
		ut.Header{Key: "dummy", Value: "b"},
	)

	// not found
	w = ut.PerformRequest(router, "GET", "/hey", nil)
	resp = w.Result()
	assert.Equal(t, 404, resp.StatusCode())

	// fake body
	w = ut.PerformRequest(router, "GET", "/hey", nil)
	_, err := w.WriteString(", faker")
	resp = w.Result()
	assert.Nil(t, err)
	assert.Equal(t, 404, resp.StatusCode())
	assert.Equal(t, "404 page not found, faker", string(resp.Body()))

	// json bind
	json := `{"version":"v0.0.1"}`
	w = ut.PerformRequest(router, "POST", "/hey/json",
		&ut.Body{Body: bytes.NewBufferString(json), Len: len(json)},
		ut.Header{Key: "Content-Type", Value: "application/json"},
	)
	resp = w.Result()
	assert.Equal(t, 200, resp.StatusCode())
}

这里面用到了两个接口:ResponseRecord 和 PerformRequest。其中获取响应用到的 Result() 函数就是第一个接口提供的,还可以用 Write(buf []byte) 来写一个串行的响应数据。然后第二个接口出现的次数很多,它用来创建一个请求,首先要定义引擎,然后使用指定的 URL,还可以添加请求的 Body 和 Header。更多的用法参考单测文档

这个例子里看着有点乱,无从下手的感觉是因为这里面把很多的情况都要覆盖进去,首先要测试全部正常的数据有什么行为,然后要测试未授权用户能有什么响应,然后测试加了特殊请求头有什么响应,然后测试 404 页面工作是否正常,最后测试一个假的请求体能有什么响应。所以整个函数里用到了 5 次创建请求获取响应,一点一点看就很清楚了。

这个测试要运行也很简单,直接:

go test unit_test/main_test.go

单测的更多例子可以参考 ut 库里的例子