【golang】28、用 httptest 做 web server 的 controller 的单测

2024-03-12 13:12

本文主要是介绍【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://www.chinasem.cn/article/801366

相关文章

JSON Web Token在登陆中的使用过程

《JSONWebToken在登陆中的使用过程》:本文主要介绍JSONWebToken在登陆中的使用过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录JWT 介绍微服务架构中的 JWT 使用结合微服务网关的 JWT 验证1. 用户登录,生成 JWT2. 自定义过滤

一文教你如何将maven项目转成web项目

《一文教你如何将maven项目转成web项目》在软件开发过程中,有时我们需要将一个普通的Maven项目转换为Web项目,以便能够部署到Web容器中运行,本文将详细介绍如何通过简单的步骤完成这一转换过程... 目录准备工作步骤一:修改​​pom.XML​​1.1 添加​​packaging​​标签1.2 添加

golang 日志log与logrus示例详解

《golang日志log与logrus示例详解》log是Go语言标准库中一个简单的日志库,本文给大家介绍golang日志log与logrus示例详解,感兴趣的朋友一起看看吧... 目录一、Go 标准库 log 详解1. 功能特点2. 常用函数3. 示例代码4. 优势和局限二、第三方库 logrus 详解1.

mysql出现ERROR 2003 (HY000): Can‘t connect to MySQL server on ‘localhost‘ (10061)的解决方法

《mysql出现ERROR2003(HY000):Can‘tconnecttoMySQLserveron‘localhost‘(10061)的解决方法》本文主要介绍了mysql出现... 目录前言:第一步:第二步:第三步:总结:前言:当你想通过命令窗口想打开mysql时候发现提http://www.cpp

SQL Server清除日志文件ERRORLOG和删除tempdb.mdf

《SQLServer清除日志文件ERRORLOG和删除tempdb.mdf》数据库再使用一段时间后,日志文件会增大,特别是在磁盘容量不足的情况下,更是需要缩减,以下为缩减方法:如果可以停止SQLSe... 目录缩减 ERRORLOG 文件(停止服务后)停止 SQL Server 服务:找到错误日志文件:删除

Windows Server服务器上配置FileZilla后,FTP连接不上?

《WindowsServer服务器上配置FileZilla后,FTP连接不上?》WindowsServer服务器上配置FileZilla后,FTP连接错误和操作超时的问题,应该如何解决?首先,通过... 目录在Windohttp://www.chinasem.cnws防火墙开启的情况下,遇到的错误如下:无法与

Spring中@RestController和@Controller的使用及区别

《Spring中@RestController和@Controller的使用及区别》:本文主要介绍Spring中@RestController和@Controller的使用及区别,具有很好的参考价... 目录Spring中@RestController和@Controller使用及区别1. 基本定义2. 使

一文详解SQL Server如何跟踪自动统计信息更新

《一文详解SQLServer如何跟踪自动统计信息更新》SQLServer数据库中,我们都清楚统计信息对于优化器来说非常重要,所以本文就来和大家简单聊一聊SQLServer如何跟踪自动统计信息更新吧... SQL Server数据库中,我们都清楚统计信息对于优化器来说非常重要。一般情况下,我们会开启"自动更新

Golang中拼接字符串的6种方式性能对比

《Golang中拼接字符串的6种方式性能对比》golang的string类型是不可修改的,对于拼接字符串来说,本质上还是创建一个新的对象将数据放进去,主要有6种拼接方式,下面小编就来为大家详细讲讲吧... 目录拼接方式介绍性能对比测试代码测试结果源码分析golang的string类型是不可修改的,对于拼接字

如何通过Golang的container/list实现LRU缓存算法

《如何通过Golang的container/list实现LRU缓存算法》文章介绍了Go语言中container/list包实现的双向链表,并探讨了如何使用链表实现LRU缓存,LRU缓存通过维护一个双向... 目录力扣:146. LRU 缓存主要结构 List 和 Element常用方法1. 初始化链表2.