【golang】28、用 httptest 做 web server 的 controller 的单测
文章目录
- 一、构建 HTTP server
- 1.1 model.go
- 1.2 server.go
- 1.3 curl 验证 server 功能
- 1.3.1 新建
- 1.3.2 查询
- 1.3.3 更新
- 1.3.4 删除
- 二、httptest 测试
- 2.1 完整示例
- 2.2 实现逻辑
- 2.3 其他示例
- 2.4 用 TestMain 避免重复的测试代码
- 2.5 gin 框架的 httptest
一、构建 HTTP server
1.1 model.go
package mainimport ("errors""time"
)var TopicCache = make([]*Topic, 0, 16)type Topic struct {Id int `json:"id"`Title string `json:"title"`Content string `json:"content"`CreatedAt time.Time `json:"created_at"`
}// 从数组中找到一项, 根据 id 找到数组的下标
func FindTopic(id int) (*Topic, error) {if err := checkIndex(id); err != nil {return nil, err}return TopicCache[id-1], nil
}// 创建一个 Topic 实例, 没有输入参数, 内部根据 Topic 数组的长度来确定新 Topic 的 id
func (t *Topic) Create() error {// 初始时len 为 0, id 为 1, 即数组下标为0时并不放置元素, 而数组从下标为1才开始放置元素t.Id = len(TopicCache) + 1 // 忽略用户传入的 id, 而是根据数组的长度, 决定此项的 Idt.CreatedAt = time.Now()TopicCache = append(TopicCache, t) // 初始时数组为空, 放入的第一个元素是 Id = 1return nil
}// 更新一个 Topic 实例, 通过 id 找到数组下标, 最终改的还是数组里的值
func (t *Topic) Update() error {if err := checkIndex(t.Id); err != nil {return err}TopicCache[t.Id-1] = treturn nil
}func (t *Topic) Delete() error {if err := checkIndex(t.Id); err != nil {return err}TopicCache[t.Id-1] = nilreturn nil
}func checkIndex(id int) error {if id > 0 && len(TopicCache) <= id-1 {return errors.New("The topic is not exists!")}return nil
}
1.2 server.go
package mainimport ("encoding/json""net/http""path""strconv"
)func main() {http.HandleFunc("/topic/", handleRequest)http.ListenAndServe(":2017", nil)
}// main handler function
func handleRequest(w http.ResponseWriter, r *http.Request) {var err errorswitch r.Method {case http.MethodGet:err = handleGet(w, r)case http.MethodPost:err = handlePost(w, r)case http.MethodPut:err = handlePut(w, r)case http.MethodDelete:err = handleDelete(w, r)}if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}
}// 获取一个帖子
// 如 GET /topic/1
func handleGet(w http.ResponseWriter, r *http.Request) error {// 用户输入的 url 中有 id, 通过 path.Base(r.URL.Path) 获取 idid, err := strconv.Atoi(path.Base(r.URL.Path))if err != nil {return err}topic, err := FindTopic(id)if err != nil {return err}// 序列化结果并输出output, err := json.MarshalIndent(&topic, "", "\t\t")if err != nil {return err}w.Header().Set("Content-Type", "application/json")w.Write(output)return nil
}// 增加一个帖子
// POST /topic/
func handlePost(w http.ResponseWriter, r *http.Request) (err error) {// 构造长度为 r.ContentLength 的缓冲区body := make([]byte, r.ContentLength)// 读取到缓冲区r.Body.Read(body)// 反序列化到对象var topic = new(Topic)err = json.Unmarshal(body, &topic)if err != nil {return}// 执行操作err = topic.Create()if err != nil {return}w.WriteHeader(http.StatusOK)return
}// 更新一个帖子
// PUT /topic/1
func handlePut(w http.ResponseWriter, r *http.Request) error {id, err := strconv.Atoi(path.Base(r.URL.Path))if err != nil {return err}topic, err := FindTopic(id)if err != nil {return err}body := make([]byte, r.ContentLength)r.Body.Read(body)json.Unmarshal(body, topic)err = topic.Update()if err != nil {return err}w.WriteHeader(http.StatusOK)return nil
}// 删除一个帖子
// DELETE /topic/1
func handleDelete(w http.ResponseWriter, r *http.Request) (err error) {id, err := strconv.Atoi(path.Base(r.URL.Path))if err != nil {return}topic, err := FindTopic(id)if err != nil {return}err = topic.Delete()if err != nil {return}w.WriteHeader(http.StatusOK)return
}
1.3 curl 验证 server 功能
1.3.1 新建
curl -i -X POST http://localhost:2017/topic/ -H 'content-type: application/json' -d '{"title":"a", "content":"b"}'HTTP/1.1 200 OK
Date: Mon, 11 Mar 2024 02:54:08 GMT
Content-Length: 0
1.3.2 查询
curl -i -X GET http://localhost:2017/topic/1HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 11 Mar 2024 03:00:11 GMT
Content-Length: 99{"id": 1,"title": "a","content": "b","created_at": "2024-03-11T10:59:44.043029+08:00"
}
1.3.3 更新
curl -i -X PUT http://localhost:2017/topic/1 -H 'content-type: application/json' -d '{"title": "c", "content": "d"}'HTTP/1.1 200 OK
Date: Mon, 11 Mar 2024 03:01:51 GMT
Content-Length: 0
curl -i -X GET http://localhost:2017/topic/1 HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 11 Mar 2024 03:01:54 GMT
Content-Length: 99{"id": 1,"title": "c","content": "d","created_at": "2024-03-11T10:59:44.043029+08:00"
}
1.3.4 删除
curl -i -X DELETE http://localhost:2017/topic/1HTTP/1.1 200 OK
Date: Mon, 11 Mar 2024 03:03:41 GMT
Content-Length: 0
curl -i -X GET http://localhost:2017/topic/1
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 11 Mar 2024 03:04:27 GMT
Content-Length: 4null
二、httptest 测试
上文,通过 curl 自测了 controller,现在通过 net/http/httptest 测试,这种测试方式其实是没有 HTTP 调用的,是通过将 handler() 函数绑定到 url 上实现的。
2.1 完整示例
package mainimport ("net/http""net/http/httptest""strings""testing"
)func TestHandlePost(t *testing.T) {// mux 是多路复用器的意思mux := http.NewServeMux()mux.HandleFunc("/topic/", handleRequest) // 将 [业务的 handleRequest() 函数] 注册到 mux 的 /topic/ 路由上// 构造一个请求reader := strings.NewReader(`{"title":"e", "content":"f"}`)r, _ := http.NewRequest(http.MethodPost, "/topic/", reader)// 构造一个响应 (httptest.ResponseRecorder 实现了 http.ResponseWriter 接口)w := httptest.NewRecorder()mux.ServeHTTP(w, r)//handleRequest(w, r)// 获取响应结果resp := w.Result()if resp.StatusCode != http.StatusOK {t.Errorf("Expected status OK; got %v", resp.Status)}
}
2.2 实现逻辑
实现逻辑如下:
首先配置路由,将 /topic 的请求都路由给 handleRequest() 函数实现。
mux := http.NewServeMux()
mux.HandleFunc("/topic/", handleRequest)
因为 handleRequest(w http.ResponseWriter, r *http.Request) 函数的签名是 w 和 r 两个参数,所以为了测试,需要构造这两个参数实例。
因为 httptest.ResponseRecorder 实现了 http.ResponseWriter 接口,所以可以用 httptest.NewRecorder() 表示 w。
准备好之后,就可以执行了
- 可以只调用 handleRequest(w, r)
- 也可以调用 mux.ServeHTTP(w, r),其内部也会调用 handleRequest(w, r),这会更完整的测试整个流程。
最后,通过 go test -v 可以执行测试。
$ go test -v
=== RUN TestHandlePost
--- PASS: TestHandlePost (0.00s)
PASS
ok benchmarkdemo 0.095s
2.3 其他示例
func TestHandleGet(t *testing.T) {mux := http.NewServeMux()mux.HandleFunc("/topic/", handleRequest)r, _ := http.NewRequest(http.MethodGet, "/topic/1", nil)w := httptest.NewRecorder()mux.ServeHTTP(w, r)resp := w.Result()if resp.StatusCode != http.StatusOK {t.Errorf("Expected status OK; got %v", resp.Status)}topic := new(Topic)json.Unmarshal(w.Body.Bytes(), topic)if topic.Id != 1 {t.Errorf("cannot get topic by id")}
}
注意,因为数据没有落地存储,为了保证后面的测试正常,请将 TestHandlePost 放在最前面。
- 如果 go test -v 测试整个包的话,TestHandlePost 和 TestHandleGet 两个单测都能成功
- 但如果分开测试的话,只有 TestHandlePost 能成功,而 TestHandleGet 会失败(因为没有 POST 创建流程,而只有 GET 创建流程的话,在业务逻辑的数组中,找不到 id = 1 的项,就会报错)
2.4 用 TestMain 避免重复的测试代码
细心的朋友应该会发现,上面的测试代码有重复,比如:
mux := http.NewServeMux()
mux.HandleFunc("/topic/", handleRequest)
以及:
w := httptest.NewRecorder()
这正好是前面学习的 setup
可以做的事情,因此可以使用 TestMain
来做重构。实现如下:
var w *httptest.ResponseRecorderfunc TestMain(m *testing.M) {w = httptest.NewRecorder()os.Exit(m.Run())
}
2.5 gin 框架的 httptest
package serviceimport ("fmt""log""net/http""net/http/httptest""strings""testing""github.com/gin-gonic/gin"
)type userINfo struct {ID uint64 `json:"id"`Name string `json:"name"`
}func handler(c *gin.Context) {var info userINfoif err := c.ShouldBindJSON(&info); err != nil {log.Panic(err)}fmt.Println(info)c.Writer.Write([]byte(`{"status": 200}`))
}func TestHandler(t *testing.T) {rPath := "/user"router := gin.Default()router.GET(rPath, handler)req, _ := http.NewRequest("GET", rPath, strings.NewReader(`{"id": "1","name": "joe"}`))w := httptest.NewRecorder()router.ServeHTTP(w, req)t.Logf("status: %d", w.Code)t.Logf("response: %s", w.Body.String())
}
相关文章:

【golang】28、用 httptest 做 web server 的 controller 的单测
文章目录 一、构建 HTTP server1.1 model.go1.2 server.go1.3 curl 验证 server 功能1.3.1 新建1.3.2 查询1.3.3 更新1.3.4 删除 二、httptest 测试2.1 完整示例2.2 实现逻辑2.3 其他示例2.4 用 TestMain 避免重复的测试代码2.5 gin 框架的 httptest 一、构建 HTTP server 1.1…...
296.【华为OD机试】污染水域 (图的多源BFS—JavaPythonC++JS实现)
🚀点击这里可直接跳转到本专栏,可查阅顶置最新的华为OD机试宝典~ 本专栏所有题目均包含优质解题思路,高质量解题代码(Java&Python&C++&JS分别实现),详细代码讲解,助你深入学习,深度掌握! 文章目录 一. 题目-污染水域二.解题思路三.题解代码Python题解代码…...

C语言——动态内存分配
前言:通过前面的学习,我们知道C语言中在内存中开辟空间的方法有:变量和数组。既然拥有了开辟空间的方法,我们为什么还要学习动态内存分配呢? int val 20; //在内存中开辟四个字节的空间 int arr[10] { 0 }; //在内…...

瑞_23种设计模式_策略模式
文章目录 1 策略模式(Strategy Pattern)★1.1 介绍1.2 概述1.3 策略模式的结构1.4 策略模式的优缺点1.5 策略模式的使用场景 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代码实现 4 JDK源码解析(Comparator) 🙊…...

使用 OpenAI 的 text-embedding 构建知识向量库并进行相似搜索
OpenAI的embedding模型的使用 首先第一篇文章中探讨和使用了ChatGPT4的API-Key实现基础的多轮对话和流式输出,完成了对GPT-API的一个初探索,那第二步打算使用OpenAI的embedding模型来构建一个知识向量库,其实知识向量库本质上就是一个包含着一…...

设计模式学习笔记 - 规范与重构 - 5.如何通过封装、抽象、模块化、中间层解耦代码?
前言 《规范与重构 - 1.什么情况下要重构?重构什么?又该如何重构?》讲过,重构可以分为大规模高层重构(简称 “大型重构”)和小规模低层次重构(简称 “小型重构”)。大型重构是对系统…...

YOLOv9实例分割教程|(二)验证教程
专栏地址:目前售价售价59.9,改进点30个 专栏介绍:YOLOv9改进系列 | 包含深度学习最新创新,助力高效涨点!!! 一、验证 打开分割验证文件,填入数据集配置文件、训练好的权重文件&…...

python 基础知识点(蓝桥杯python科目个人复习计划63)
今日复习内容:做题 例题1:蓝桥骑士 问题描述: 小蓝是蓝桥王国的骑士,他喜欢不断突破自我。 这天蓝桥国王给他安排了N个对手,他们的战力值分别为a1,a2,...,an,且按顺序阻挡在小蓝的前方。对于这些对手小…...

IAB视频广告标准《数字视频和有线电视广告格式指南》之 简介、目录及视频配套广告 - 我为什么要翻译介绍美国人工智能科技公司IAB系列(2)
写在前面 谈及到中国企业走入国际市场,拓展海外营销渠道的时候,如果单纯依靠一个小公司去国外做广告,拉渠道,找代理公司,从售前到售后,都是非常不现实的。我们可以回想一下40年前,30年前&#x…...

Python网络基础爬虫-python基本语法
文章目录 逻辑语句if,else,elifforwhile异常处理 函数与类defpassclass 逻辑语句 熟悉C/C语言的人们可能很希望Python提供switch语句,但Python中并没有这个关键词,也没有这个语句结构。但是可以通过if-elif-elif-…这样的结构代替,或者使用字…...

产品推荐 - 基于星嵌 OMAPL138+国产FPGA的DSP+ARM+FPGA三核开发板
1 评估板简介 基于TI OMAP-L138(定点/浮点DSP C674xARM9) FPGA处理器的开发板; OMAP-L138是TI德州仪器的TMS320C6748ARM926EJ-S异构双核处理器,主频456MHz,高达3648MIPS和2746MFLOPS的运算能力; FPGA…...

【微服务学习笔记(一)】Nacos、Feign、Gateway基础使用
【微服务学习笔记(一)】Nacos、Feign、Gateway基础使用 总览Nacos安装配置Nacos注册中心服务多级存储模型负载均衡规则环境隔离 配置管理配置拉取配置热更新多服务共享配置 Feign远程调用配置性能优化Fegin使用 统一网关Gateway搭建网关路由断言工厂&…...

使用maven打生产环境可执行包
一、程序为什么要打包 程序打包的主要目的是将项目的源代码、依赖库和其他资源打包成一个可执行的文件或者部署包,方便程序的发布和部署。以下是一些打包程序的重要理由: 方便部署和分发:打包后的程序可以作为一个独立的实体,方便…...

springboot+ssm基于vue.js的客户关系Crm管理系统
系统包含两种角色:管理员、用户,主要功能如下。 ide工具:IDEA 或者eclipse 编程语言: java 数据库: mysql5.7 框架:ssmspringboot都有 前端:vue.jsElementUI 详细技术:springbootSSMvueMYSQLMAVEN 数据库…...

github 中的java前后端项目整合到本地运行
前言: 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 本文章未…...
分布式ID(7):Zookeeper实现分布式ID生成
1 原理 实现方式有两种,一种通过节点,一种通过节点的版本号 节点的特性持久顺序节点(PERSISTENT_SEQUENTIAL) 他的基本特性和持久节点是一致的,额外的特性表现在顺序性上。在ZooKeeper中,每个父节点都会为他的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序…...

钉钉小程序 - - - - - 如何通过一个链接打开小程序内的指定页面
方式1 钉钉小程序 scheme dingtalk://dingtalkclient/action/open_mini_app?miniAppId123&pagepages%2Findex%2Findex%3Fx%3D%25E4%25B8%25AD%25E6%2596%2587 方式2 https://applink.dingtalk.com/action/open_mini_app?type2&miniAppIdminiAppId&corpIdcorpId&…...

Java代码基础算法练习---2024.3.14
其实这就是从我学校的资源,都比较基础的算法题,先尽量每天都做1-2题,练手感。毕竟离我真正去尝试入职好的公司(我指的就是中大厂,但是任重道远啊),仍有一定的时间,至少要等我升本之后…...
3月14日,每日信息差
🎖 素材来源官方媒体/网络新闻 🎄 5.5G通信网络在海南投入商用,较5G提升10倍 🌍 国务院批复同意,珠海港口岸将整合并扩大开放 🌋 同有科技:正在研究新型磁电存储技术 🎁 美国折扣零售…...

学习Android的第二十八天
目录 Android Service (服务) 线程 Service (服务) Service 相关方法 Android 非绑定 Service startService() 启动 Service 验证 startService() 启动 Service 的调用顺序 Android 绑定 Service bindService() 启动 Service 验证 BindService 启动 Service 的顺序 …...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...

stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
【算法训练营Day07】字符串part1
文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接:344. 反转字符串 双指针法,两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...