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

news2024/11/18 7:42:19

文章目录

  • 一、构建 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())
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何配置固定TCP公网地址实现远程访问内网MongoDB数据库

文章目录 前言1. 安装数据库2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射2.3 测试随机公网地址远程连接 3. 配置固定TCP端口地址3.1 保留一个固定的公网TCP端口地址3.2 配置固定公网TCP端口地址3.3 测试固定地址公网远程访问 前言 MongoDB是一个基于分布式文件存储的数…

JDK环境变量配置-jre\bin、rt.jar、dt.jar、tools.jar

我们主要看下rt.jar、dt.jar、tools.jar的作用&#xff0c;rt.jar在​%JAVA_HOME%\jre\lib&#xff0c;dt.jar和tools.jar在%JAVA_HOME%\lib下。 rt.jar&#xff1a;Java基础类库&#xff0c;也就是Java doc里面看到的所有的类的class文件。 tools.jar&#xff1a;是系统用来编…

星星魔方

星星魔方 1&#xff0c;魔方三要素 &#xff08;1&#xff09;组成部件 6个中心块和8个角块和三阶魔方同构&#xff0c;另外每个面还有构成五角星的十个块。 &#xff08;2&#xff09;可执行操作 一共12种操作&#xff0c;其中6种是每个层顺时针旋转90度&#xff0c;另外6…

Gateway(路由映射)

1.SpringCloud Gateway Spring Cloud Gateway组件的核心是一系列的过滤器&#xff0c;通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务。 Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器&#xff0c;隐藏微服务结点IP端口信息&#xff0c;从而加…

用Vision Pro来控制机器人

【技术框架概述】 - visionOS App + Python Library用于从Vision Pro将头部/手腕/手指跟踪数据流式传输到任何机器人。 【定位】 - 该框架旨在利用Vision Pro控制机器人,并记录用户在环境中导航和操作的方式,以训练机器人。 【核心功能】 1. 提供visionOS应用程序和Py…

TEASEL: A transformer-based speech-prefixed language model

文章目录 TEASEL&#xff1a;一种基于Transformer的语音前缀语言模型文章信息研究目的研究内容研究方法1.总体框图2.BERT-style Language Models&#xff08;基准模型&#xff09;3.Speech Module3.1Speech Temporal Encoder3.2Lightweight Attentive Aggregation (LAA) 4.训练…

大语言模型系列-中文开源大模型

文章目录 前言一、主流开源大模型二、中文开源大模型排行榜 前言 近期&#xff0c;OpenAI 的主要竞争者 Anthropic 推出了他们的新一代大型语言模型 Claude 3&#xff0c;该系列涵盖了三个不同规模的模型&#xff1a;Opus、Sonnet 和 Haiku。 Claude 3声称已经全面超越GPT-4。…

软考71-上午题-【面向对象技术2-UML】-UML中的图2

一、用例图 上午题&#xff0c;考的少&#xff1b;下午题&#xff0c;考的多。 1-1、用例图的定义 用例图展现了一组用例、参与者以及它们之间的关系。 用例图用于对系统的静态用例图进行建模。 可以用下列两种方式来使用用例图&#xff1a; 1、对系统的语境建模&#xff1b…

人口性别年龄分布数据、不同年龄结构、性别结构人口分布数据、乡镇街道人口分布数据

人口分布是指人口在一定时间内的空间存在形式、分布状况&#xff0c;包括各类地区总人口的分布&#xff0c;以及某些特定人口&#xff08;如城市人口、、特定的人口过程和构成&#xff08;如迁移、性别等&#xff09;的分布等。 人口分布的最大特征是不平衡性。就全世界而言&am…

【工具】软件工具分享哪家强?安卓apk安装软件分享新方法,弃用QQ启用企业微信使用方法...

微信关注公众号 “DLGG创客DIY” 设为“星标”&#xff0c;重磅干货&#xff0c;第一时间送达。 前言 又又来聊软件工具分享 先简单回顾一下之前的内容&#xff1a; 按时间先后顺序&#xff1a; 1.从网盘到QQ群文件及群文件分类 【工具】软件工具分享哪家强&#xff1f;群文件使…

Mac电脑搭建前端项目环境,并适配老项目

1.上一篇文章中&#xff0c;我说到了&#xff0c;node.js中文网下载node 包&#xff0c;根据系统进行选择&#xff0c;然后安装包node即可&#xff0c;对于比较新的项目确实也是适用的&#xff0c;但是老项目就不行了会报错&#xff0c;node版本过高&#xff0c;导致环境不匹配…

Java线程的基本操作

线程的基本操作 Java线程的常用操作都定义在Thread类中&#xff0c;包括一些重要的静态方法 和线程的实例方法 。下面我们来学习一下&#xff0c;线程的常用基本操作 1.线程名称的设置和获取 线程名称可以通过构造Thread的时候进行设置&#xff0c;也可以通过实例的方法setName…

科技云报道:两会热议的数据要素,如何拥抱新技术?

科技云报道原创。 今年全国两会上&#xff0c;“数字经济”再次成为的热点话题。 2024年政府工作报告提到&#xff1a;要健全数据基础制度&#xff0c;大力推动数据开发开放和流通使用&#xff1b;适度超前建设数字基础设施&#xff0c;加快形成全国一体化算力体系&#xff1…

【Flutter】报错Target of URI doesn‘t exist ‘package:flutter/material.dart‘

运行别人项目 包无法导入报错&#xff1a;Target of URI doesn’t exist ‘package:flutter/material.dart’ 解决方法 flutter packages get成功 不会报错

Centos本地、公网邮件发送配置

目录 本地邮件发送 发送邮件的三种方式 接受邮件 配置公网发送邮件 发送文件 本地邮件发送 安装服务 # yum -y install postfix # yum -y install mailx 启动服务 # systemctl start postfix 发送邮件的三种方式 一. # mail-s“邮件主题” 收件人 ​ 邮件内容…

Linux - 安装 Jenkins(详细教程)

目录 前言一、简介二、安装前准备三、下载与安装四、配置镜像地址五、启动与关闭六、常用插件的安装 前言 虽然说网上有很多关于 Jenkins 安装的教程&#xff0c;但是大部分都不够详细&#xff0c;或者是需要搭配 docker 或者 k8s 等进行安装&#xff0c;对于新手小白而已&…

智谱清华LongAlign发布:重塑NLP长文本处理

引言 随着大型语言模型&#xff08;LLMs&#xff09;的不断进化&#xff0c;我们现在能够处理的文本长度已经达到了前所未有的规模——从最初的几百个tokens到现在的128k tokens&#xff0c;相当于一本300页的书。这一进步为语义信息的提供、错误率的减少以及用户体验的提升打…

解决方案RuntimeError: CUDA out of memory

文章目录 一、现象&#xff1a;二、解决方案 一、现象&#xff1a; PyTorch深度学习框架&#xff0c;运行bert-mini&#xff0c;本地环境是torch1.4-gpu&#xff0c;发现报错显示&#xff1a;RuntimeError: CUDA out of memory. Tried to allocate 224.00 MiB (GPU 0; 15.89 G…

保护物联网设备免受网络攻击的方法

可以肯定地说&#xff0c;物联网设备让我们的生活变得更加轻松和方便。这项新技术改变了人们在办公室工作的方式&#xff0c;也改变了他们在家里使用小工具的方式。办公室或家里的所有智能设备都可以连接&#xff0c;这让生活变得更加轻松。然而&#xff0c;这也使这些设备面临…

【Redis学习_可视化客户端连接Redis】

Redis学习_可视化客户端连接Redis Redis学习_可视化客户端连接Redis1、Another Redis Desktop Manager介绍2、Another Redis Desktop Manager连接 Redis学习_可视化客户端连接Redis 1、Another Redis Desktop Manager介绍 介绍 Another Redis Desktop Manager 支持哨兵, 集群,…