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

推荐订阅源

N
News | PayPal Newsroom
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
大猫的无限游戏
大猫的无限游戏
GbyAI
GbyAI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LangChain Blog
S
SegmentFault 最新的问题
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Project Zero
Project Zero
P
Privacy & Cybersecurity Law Blog
V
Vulnerabilities – Threatpost
博客园 - 三生石上(FineUI控件)
Recorded Future
Recorded Future
The Hacker News
The Hacker News
C
CXSECURITY Database RSS Feed - CXSecurity.com
C
CERT Recently Published Vulnerability Notes
宝玉的分享
宝玉的分享
aimingoo的专栏
aimingoo的专栏
T
Tor Project blog
T
The Exploit Database - CXSecurity.com
Schneier on Security
Schneier on Security
H
Help Net Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
M
MIT News - Artificial intelligence
W
WeLiveSecurity
P
Proofpoint News Feed
A
About on SuperTechFans
S
Securelist
I
InfoQ
G
Google Developers Blog
博客园 - 司徒正美
博客园 - 叶小钗
Latest news
Latest news
F
Fortinet All Blogs
G
GRAHAM CLULEY
腾讯CDC
Jina AI
Jina AI
S
Schneier on Security
I
Intezer
V
Visual Studio Blog
美团技术团队
V2EX - 技术
V2EX - 技术
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
罗磊的独立博客
Y
Y Combinator Blog

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项目重构小结 工作七年小结: 学习,生活及其他 [分享]bash日常: bash-utils 极客时间推广海报 2017总结: 予时光以意义 k8s APIServer源码: api注册详细细节 k8s APIServer源码: api注册主体流程 k8s APIServer源码: 服务启动 k8s APIServer源码: go-restful框架 重构 - 读书笔记(Python示例) 写给新人的沟通建议 vim 杂谈 - 关于快速编辑 vim 杂谈 - 关于移动 读书笔记-重构: 章11 处理概括关系 读书笔记-重构: 章10 简化函数调用 读书笔记-重构: 章9 简化表达式 读书笔记-重构: 章8 重新组织数据 读书笔记-重构: 章7 在对象之间搬移特性 读书笔记-重构: 章6 重新组织函数 Python 代码规范小结 [分享]关于vim ElasticSearch集群部署文档 Logstash+ElasticSearch处理mysql慢查询日志 [分享]关于代码调试DE那些事 Logstash+ElasticSearch+Kibana- 实现相对通用的数据收集分析 ELK维护的一些点(二) [分享]Python源码剖析-数据结构 一些Centos Python生产环境的部署命令 摘录<<6个月学会任何一种外语>> ELK 维护的一些点 也许是一个新的开始 一些vim的个性化配置 读书笔记-调试九法 这段时间的一些想法 Python 源码阅读 - 垃圾回收机制 我为什么要写博客 APUE笔记-第一章 UNIX基础知识 Python源码阅读-闭包的实现 Python源码阅读-内存管理机制(二) Python源码阅读-内存管理机制(一) Python-基础-数据结构小结 '活动'设计的一些trick 一些简单的Python测试题 我的tmux配置及说明【k-tmux】 Review and Restart 工作四周年小结 vim插件: surround & repeat[成对符号编辑] vim插件: gundo[时光机] vim插件: expand-region[区域选中] vim插件: quickrun[快速执行] vim插件: trailing-whitespace[行尾空格处理] vim插件: closetag[成对标签补全] vim插件: ctrlp[文件搜索] vim插件: airline[状态栏增强] vim插件: theme[主题] vim插件: tagbar[大纲式导航] vim插件: nerdcommenter[快速注释] vim插件: rainbow_parentheses[括号高亮] vim插件: syntastic[语法检查] vim插件: delimitmate[符号自动补全] vim插件: matchit[成对标签跳转] vim插件: easy-align[快速对齐] vim插件: multiple-cursors[多光标操作] vim插件: vim-signature[快速标记跳转] vim插件: easymotion[快速跳转] vim插件: vundle[管理插件] Elasticsearch几个问题的解决 分享一份 Vim 简介PPT k-vim 更新9.0版本 关于知识管理工具的思考 Logstash+ElasticSearch+Kibana处理nginx访问日志
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()

}