惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
爱范儿
爱范儿
V
Visual Studio Blog
Engineering at Meta
Engineering at Meta
B
Blog
云风的 BLOG
云风的 BLOG
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
U
Unit 42
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
WordPress大学
WordPress大学
D
DataBreaches.Net
B
Blog RSS Feed
小众软件
小众软件
人人都是产品经理
人人都是产品经理
I
InfoQ
P
Proofpoint News Feed
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
The GitHub Blog
The GitHub Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报

Posts on WKLKEN THINKING

apisix 中的 lrucache apisix 中的服务发现机制 apisix 中的负载均衡 apisix etcd机制 聊聊框架 关于 k8s 的 zero downtime deployment 一些建议 apisix 遇到的一些问题 关于在除夕前一天换了一个洗衣机的故事 Django DRF 性能优化 DRF 的一些实践 Part1: Serializer DRF继承关系图 Better Code: 关于接口的灵活性 新的仓库: wklken/naming 缓存使用的一些经验 Better Code: 抽象: 可扩展性与可维护性的抉择 Better Code: 异常时, 该提示用户哪些信息? Better Code: 更好的异常日志打印 Go: some libs Go: go-redis/cache升级的坑 Go: logrus性能提升 Go: gin validation 远程办公的一点总结 Go: 开发过程中的一些bug 项目管理实践: 风险驱动开发 Go: 一种error wrap调用链处理方式 漫谈技术选型 Go: go-sql-driver interpolateparams参数优化 [分享]深度工作 你需要更多的思考时间 Django项目重构小结
Go: 基于 apitest 做handler层单元测试
2021-01-22 · via Posts on WKLKEN THINKING

A simple and extensible behavioural testing library. Supports mocking external http calls and renders sequence diagrams on completion. Credit to testify which is this libraries’ only dependency

相关资源:

老的方案: postman/newman

原先使用 postman + newman 的方式做的 API 功能测试;

这种方式不完全是API层的单元测试, 预先配置好数据库/初始化好数据后, 直接通过API调用, 判定响应值

是介于单元测试 + 集成测试 之间的一种测试, 但是既不是完备的单元测试, 也不是完整的集成测试;

好处:

  • 简单方便, 在本地开发调试API时, 就能将调试请求直接转为测试用例

缺点:

  • 难以维护: 更新API/增加API时, 未实时同步更新测试用例; 变更上层协议时, 例如middleware, 所有case需要改
  • 违背Isolate原则: 无法独立, 每个case运行时, 前面执行的数据会影响后面执行的数据; 每个case都需要考虑规避之前case的数据影响
  • 缺乏完备性: 一个API一般只构造一个失败, 一个成功两个Case

新的方案

  • Golang 使用 apitest 做handler层的单元测试
  • 当前只覆盖核心逻辑
  • 重新去寻找集成测试的方案

1. 入门示例

func TestApi(t *testing.T) {
	apitest.New().
		Handler(handler).
		Get("/user/1234").
		Expect(t).
		Body(`{"id": "1234", "name": "Tate"}`).
		Status(http.StatusCreated).
		End()
}

2. 传统httptest vs apitest

使用httptest

func TestPong(t *testing.T) {
	r := util.SetupRouter()
	r.GET("/test", Pong)

	req, _ := http.NewRequest("GET", "/test", nil)
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	assert.Equal(t, 200, w.Code)

	body, err := ioutil.ReadAll(w.Body)
	assert.NoError(t, err)
	assert.True(t, strings.Contains(string(body), "pong"))
}

使用apitest

func TestPong(t *testing.T) {
	r := util.SetupRouter()
	r.GET("/ping", Pong)

	apitest.New().
		Handler(r).
		Get("/ping").
		Expect(t).
		Body(`{"message":"pong"}`).
		Status(http.StatusOK).
		End()
}

3. why not JSONpath?

我们决定禁止在测试中使用JSONpath, 虽然这是apitest提供的一个强大的特性

官方示例: {"a": 12345, "b": [{"key": "c", "value": "result"}]}

func TestApi(t *testing.T) {
	apitest.New().
		Handler(handler).
		Get("/hello").
		Expect(t).
		Assert(jsonpath.Contains(`$.b[? @.key=="c"].value`, "result")).
		End()
}

问题点: 可维护性并不好(多一层学习和理解成本, JSONPath本身是一套很强大的语法, 但也就意味着其复杂度很高); 而单元测试最重要的就是后续的可维护性

4. JSONAssert: 封装 json 解析成map[string]interface

注意力集中在json body, 而不是解析

type JSONAssertFunc func(map[string]interface{}) error

func NewJSONAssertFunc(t *testing.T, assertFunc JSONAssertFunc) func(res *http.Response, req *http.Request) error {
	return func(res *http.Response, req *http.Request) error {
		body, err := ioutil.ReadAll(res.Body)
		assert.NoError(t, err, "read body from response fail")

		defer res.Body.Close()

		var data map[string]interface{}
		//var data Response

		err = json.Unmarshal(body, &data)
		assert.NoError(t, err, "unmarshal string to json fail")

		return assertFunc(data)
	}
}

使用

func TestVersion(t *testing.T) {
	r := util.SetupRouter()
	r.GET("/version", Version)

	apitest.New().
		Handler(r).
		Get("/version").
		Expect(t).
		Assert(util.NewJSONAssertFunc(t, func(m map[string]interface{}) error {
			assert.Contains(t, m, "version")
			assert.Contains(t, m, "commit")
			assert.Contains(t, m, "buildTime")
			assert.Contains(t, m, "goVersion")
			assert.Contains(t, m, "env")
			return nil
		})).
		Status(http.StatusOK).
		End()
}

5. ResponseAssert: 封装成后台返回协议(Response)

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

type ResponseAssertFunc func(Response) error

func NewResponseAssertFunc(
	t *testing.T,
	responseFunc ResponseAssertFunc,
) func(res *http.Response, req *http.Request) error {
	return func(res *http.Response, req *http.Request) error {
		body, err := ioutil.ReadAll(res.Body)
		assert.NoError(t, err, "read body from response fail")

		defer res.Body.Close()

		var data Response

		err = json.Unmarshal(body, &data)
		assert.NoError(t, err, "unmarshal string to response fail")

		return responseFunc(data)
	}
}

使用

func TestCreateSystemBadRequestInvalidJson(t *testing.T) {
	r := util.SetupRouter()
	url := "/api/v1/systems"
	r.POST(url, CreateSystem)
	// validate fail
	apitest.New().
		Handler(r).
		Post(url).
		JSON(map[string]interface{}{
			"hello": "123",
		}).
		Expect(t).
		Assert(util.NewResponseAssertFunc(t, func(resp util.Response) error {
			assert.Equal(t, resp.Code, util.BadRequestError)
			assert.Equal(t, resp.Message, "bad request:ID is required")
			return nil
		})).
		Status(http.StatusOK).
		End()
}

6. 能不能整合 ginkgo?

apitst的Expect方法接收的是 *testing.T, 对应interface是testing.TB

// Expect marks the request spec as complete and following code will define the expected response
func (r *Request) Expect(t *testing.T) *Response {
	r.apiTest.t = t
	return r.apiTest.response
}

并不兼容于 ginkgo 的GinkgoT()

7. 再抽象一层? No

不要再试图抽象成一个函数, 为了足够的灵活性, 势必导致函数参数数量增多, 导致使用困难;

最终完备的函数封装跟链式调用已经没有区别了

问题复杂化了. 再次封装的意义不大

func assertRequestResponse(
	t *testing.T,
	r http.Handler,
	url string,
	appCode, appSecret string,
	jsonData map[string]interface{},
	assertFunc func(res *http.Response, req *http.Request) error) {

	// 问题:
	// 1. post/get/put/delete
	// 2. headers
	// 3. query参数等等

	apitest.New().
		Handler(r).
		Post(url).
		Header("X-App-Code", appCode).
		Header("X-App-Secret", appSecret).
		JSON(
			jsonData,
		).
		Expect(t).
		Assert(
			assertFunc,
		).
		Status(http.StatusOK).
		End()

}