【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 的顺序 …...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

visual studio 2022更改主题为深色
visual studio 2022更改主题为深色 点击visual studio 上方的 工具-> 选项 在选项窗口中,选择 环境 -> 常规 ,将其中的颜色主题改成深色 点击确定,更改完成...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...

2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面
代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

从零实现STL哈希容器:unordered_map/unordered_set封装详解
本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说,直接开始吧! 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...

SpringTask-03.入门案例
一.入门案例 启动类: package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...

使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...