【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 main

import (
	"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, 而是根据数组的长度, 决定此项的 Id
	t.CreatedAt = time.Now()
	TopicCache = append(TopicCache, t) // 初始时数组为空, 放入的第一个元素是 Id = 1
	return nil
}

// 更新一个 Topic 实例, 通过 id 找到数组下标, 最终改的还是数组里的值
func (t *Topic) Update() error {
	if err := checkIndex(t.Id); err != nil {
		return err
	}
	TopicCache[t.Id-1] = t
	return nil
}

func (t *Topic) Delete() error {
	if err := checkIndex(t.Id); err != nil {
		return err
	}
	TopicCache[t.Id-1] = nil
	return 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 main

import (
	"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 error
	switch 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) 获取 id
	id, 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/1

HTTP/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/1
                                                                   
HTTP/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: 4

null

二、httptest 测试

上文,通过 curl 自测了 controller,现在通过 net/http/httptest 测试,这种测试方式其实是没有 HTTP 调用的,是通过将 handler() 函数绑定到 url 上实现的。

2.1 完整示例

package main

import (
	"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.ResponseRecorder

func TestMain(m *testing.M) {
	w = httptest.NewRecorder()
	os.Exit(m.Run())
}

2.5 gin 框架的 httptest

package service

import (
	"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 userINfo
	if 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())
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/449445.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[密码学]Base64编码

一、相关指令 1. 查看工具版本号 base64 --version2. 对字符串加密 echo 字符串 | base64 echo "Hello base64" | base643. 对字符串解密 echo 字符串 |base64 -d echo "SGVsbG8gTGV0aWFuLVJTQQo" | base64 -d4. 对文件加密 base64 文件名 base64 tex…

Linux Centos系统 磁盘分区和文件系统管理 (深入理解)

CSDN 成就一亿技术人&#xff01; 作者主页&#xff1a;点击&#xff01; Linux专栏&#xff1a;点击&#xff01; CSDN 成就一亿技术人&#xff01; 前言———— 磁盘 在Linux系统中&#xff0c;磁盘是一种用于存储数据的物理设备&#xff0c;可以是传统的硬盘驱动器&am…

个人商城系统开源(配置支付宝支付!)

原文地址&#xff1a;个人商城系统开源&#xff08;配置支付宝支付&#xff01;&#xff09; - Pleasure的博客 下面是正文内容&#xff1a; 前言 由于近期实在没有什么话题可写和一些有趣的项目教程可以分享。所以我只能决定将我自己亲手编写的一个迷你迷你商城系统进行开源…

HYBBS 表白墙网站PHP程序源码,支持封装成APP

PHP表白墙网站源码&#xff0c;适用于校园内或校区间使用&#xff0c;同时支持封装成APP。告别使用QQ空间的表白墙。 简单安装&#xff0c;只需PHP版本5.6以上即可。 通过上传程序进行安装&#xff0c;并设置账号密码&#xff0c;登录后台后切换模板&#xff0c;适配手机和PC…

软考 系统架构设计师之回归及知识点回顾(6)

接前一篇文章&#xff1a;软考 系统架构设计师之回归及知识点回顾&#xff08;5&#xff09; 10. 边缘计算 边云协同 边缘计算与云计算各有所长&#xff0c;云计算擅长全局性、非实时、长周期的大数据处理与分析&#xff0c;能够在长周期维护、业务决策支撑等领域发挥优势&…

【汇编】#4 8086与转移地址有关有关的寻址方式

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、CS与IP功能tips&#xff1a;CS、IP复位值 二、修改CS与IP的指令1. jmp指令 三、与转移地址有关的寻址方式1、段内转移1.1 段内直接寻址1.2 段内间接寻址 2. 段间…

RK3588-PCIe

1. 简介 PCIe&#xff08;Peripheral Component Interconnect Express&#xff09;是一种用于连接主板和外部设备的高速串行接口标准。它是 PCI 技术的后继者&#xff0c;旨在提供更高的带宽和更好的性能。 高速传输&#xff1a; PCIe接口提供了高速的数据传输通道&#xff0…

AHU 汇编 实验四

实验名称&#xff1a;实验四 两个数的相乘 实验内容&#xff1a; 用子程序形式编写&#xff1a; A*B&#xff1a;从键盘输入a和b&#xff0c;计算A*B&#xff0c;其中乘法采用移位和累加完成 实验过程&#xff1a; 源代码&#xff1a; data segmentmul1 db 16,?,16 dup(?…

Jenkins cron定时构建触发器

from&#xff1a; https://www.jenkins.io/doc/book/pipeline/syntax/#cron-syntax 以下内容为根据Jenkins官方文档cron表达式部分翻译过来&#xff0c;使用机翻加个人理解补充内容&#xff0c;包括举例。 目录 介绍举例&#xff1a;设置方法方法一&#xff1a;方法二&#xf…

web | http 的一些问题 | get/post的区别 | http版本 | http与https的区别 | session、cookie、token

怎么来说呢&#xff1f;这应该算一个大类了&#xff0c;基本上设计网络的应用层 当然重要的是从网络层----->应用层 &#xff08;杠精勿杠&#xff0c;知道中间还有其他层&#xff09; 先来讲一下http的结构 都知道http 有三部分&#xff0c;头部、请求头和body 头部&#x…

SQLiteC/C++接口简介

上一篇&#xff1a;SQLite——世界上部署最广泛的开源数据库&#xff08;简介&#xff09; 下一篇&#xff1a;SQLiteC/C接口详细介绍&#xff08;一&#xff09; 引言&#xff1a; 作为一种轻量级、嵌入式关系型数据库&#xff0c;SQLite已经成为许多应用和系统的首选解决方…

Discord OAuth2授权以及机器人监听群事件

下面文章讲解获取OAuth2授权整个流程&#xff0c;创建机器人&#xff0c;使用机器人监听工会&#xff08;工会就是创建的服务器&#xff09;成员变化等等&#xff0c;对接国外的都是需要VPN的哦&#xff0c;对接的时候记得提前准备。 创建应用 点击 此页面添加应用,&#xff…

使用kettle批量加载数据到kadb

测试环境 达梦数据库版本&#xff1a;DM Database Server 64 V8 03134284132-20240115-215128-20081&#xff08;官网测试版&#xff09;KADB版本&#xff1a;KADB V003R002C001B0181Kettle版本&#xff1a;pdi-ce-9.4.0.0-343&#xff08;官网下载&#xff09;Python版本&…

解释“RNN encode-decode”

“RNN encode-decode” 涉及使用循环神经网络&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;来执行编码和解码操作。这种结构常用于处理序列数据&#xff0c;例如自然语言处理、语音识别和时间序列预测等任务。 以下是 “RNN encode-decode” 的一般概念&a…

Flink实操:Flink SQL实现SFTP文件读写操作

一、背景 公司需要将Doris数据库中的部分表数据同步至SFTP服务器&#xff0c;以供其他合作企业安全读取和使用。目前&#xff0c;平台数据同步功能统一使用Flink引擎进行实时同步、离线同步的工作。因此&#xff0c;希望能够充分利用现有的Flink引擎&#xff0c;并将其复用于这…

四 超级数据查看器 讲解稿 列表功能1

四 超级数据查看器 讲解稿 列表功能1 点击此处 以新页面 打开B站 播放教学视频 APP下载地址 百度手机助手 下载地址4 讲解稿全文&#xff1a; 大家好&#xff0c;今天我们讲解一下&#xff0c;超级数据查看器列表界面&#xff0c;分为1-2两集。 首先&#xff0c…

ChatGPT+MATLAB应用

MatGPT是一个由chatGPT类支持的MATLAB应用程序&#xff0c;由官方Toshiaki Takeuchi开发&#xff0c;允许您轻松访问OpenAI提供的chatGPT API。作为官方发布的内容&#xff0c;可靠性较高&#xff0c;而且也是完全免费开源的&#xff0c;全程自己配置&#xff0c;无需注册码或用…

MySQL的加锁规则

学习了MySQL的锁后&#xff0c;知道其有这么多锁&#xff0c;那应该会有些疑惑&#xff0c;这么多锁&#xff0c;究竟我在写sql语句时候用到哪个锁的&#xff0c;什么情况是用什么锁的&#xff1f;在哪里查看该sql语句是用了哪些锁的呢&#xff1f;加锁的规则是什么呢&#xff…

【C++初阶】第六站 : 模板初阶

前言&#xff1a; 本章知识点&#xff1a;泛型编程、函数模板、类模板 专栏&#xff1a; C初阶 目录 泛型编程 函数模板 1.函数模板概念 2.函数模板格式 3.函数模板的原理 4.函数模板的实例化 5.模板参数的匹配原则 类模板 类模板的定义格式 类模板的实例化 泛型编程 如何实现一…