首页 > 其他分享 >Go 单元测试之HTTP请求与API测试

Go 单元测试之HTTP请求与API测试

时间:2024-04-17 17:11:41浏览次数:30  
标签:HTTP 请求 gock 单元测试 API gin msg http com

目录

一、httptest

1.1 前置代码准备

假设我们的业务逻辑是搭建一个http server端,对外提供HTTP服务。用来处理用户登录请求,用户需要输入邮箱,密码。

package main

import (
	regexp "github.com/dlclark/regexp2"
	"github.com/gin-gonic/gin"
	"net/http"
)

type UserHandler struct {
	emailExp    *regexp.Regexp
	passwordExp *regexp.Regexp
}

func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
	ug := server.Group("/user")
	ug.POST("/login", u.Login)
}
func NewUserHandler() *UserHandler {
	const (
		emailRegexPattern    = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
		passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
	)
	emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)
	passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)
	return &UserHandler{
		emailExp:    emailExp,
		passwordExp: passwordExp,
	}
}

type LoginRequest struct {
	Email string `json:"email"`
	Pwd   string `json:"pwd"`
}

func (u *UserHandler) Login(ctx *gin.Context) {
	var req LoginRequest
	if err := ctx.ShouldBindJSON(&req); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"msg": "参数不正确!"})
		return
	}

	// 校验邮箱和密码是否为空
	if req.Email == "" || req.Pwd == "" {
		ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱或密码不能为空"})
		return
	}

	// 正则校验邮箱
	ok, err := u.emailExp.MatchString(req.Email)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "系统错误!"})
		return
	}
	if !ok {
		ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱格式不正确"})
		return
	}

	// 校验密码格式
	ok, err = u.passwordExp.MatchString(req.Pwd)
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"msg": "系统错误!"})
		return
	}
	if !ok {
		ctx.JSON(http.StatusBadRequest, gin.H{"msg": "密码必须大于8位,包含数字、特殊字符"})
		return
	}

	// 校验邮箱和密码是否匹配特定的值来确定登录成功与否
	if req.Email != "[email protected]" || req.Pwd != "hello#world123" {
		ctx.JSON(http.StatusBadRequest, gin.H{"msg": "邮箱或密码不匹配!"})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"msg": "登录成功!"})
}

func InitWebServer(userHandler *UserHandler) *gin.Engine {
	server := gin.Default()
	userHandler.RegisterRoutes(server)
	return server
}

func main() {
	uh := &UserHandler{}
	server := InitWebServer(uh)
	server.Run(":8080") // 在8080端口启动服务器
}

1.2 介绍

在 Web 开发场景下,单元测试经常需要模拟 HTTP 请求和响应。使用 httptest 可以让我们在测试代码中创建一个 HTTP 服务器实例,并定义特定的请求和响应行为,从而模拟真实世界的网络交互,在Go语言中,一般都推荐使用Go标准库 net/http/httptest 进行测试。

1.3 基本用法

使用 httptest 的基本步骤如下:

  1. 导入 net/http/httptest 包。
  2. 创建一个 httptest.Server 实例,并指定你想要的服务器行为。
  3. 在测试代码中使用 httptest.NewRequest 创建一个模拟的 HTTP 请求,并将其发送到 httptest.Server
  4. 检查响应内容或状态码是否符合预期。

以下是一个简单的 httptest 用法示例

package main

import (
	"bytes"
	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestUserHandler_Login(t *testing.T) {
	// 定义测试用例
	testCases := []struct {
		name     string
		reqBody  string
		wantCode int
		wantBody string
	}{
		{
			name:     "登录成功",
			reqBody:  `{"email": "[email protected]", "pwd": "hello#world123"}`,
			wantCode: http.StatusOK,
			wantBody: `{"msg": "登录成功!"}`,
		},
		{
			name:     "参数不正确",
			reqBody:  `{"email": "[email protected]", "pwd": "hello#world123",}`,
			wantCode: http.StatusBadRequest,
			wantBody: `{"msg": "参数不正确!"}`,
		},
		{
			name:     "邮箱或密码为空",
			reqBody:  `{"email": "", "pwd": ""}`,
			wantCode: http.StatusBadRequest,
			wantBody: `{"msg": "邮箱或密码不能为空"}`,
		},
		{
			name:     "邮箱格式不正确",
			reqBody:  `{"email": "invalidemail", "pwd": "hello#world123"}`,
			wantCode: http.StatusBadRequest,
			wantBody: `{"msg": "邮箱格式不正确"}`,
		},
		{
			name:     "密码格式不正确",
			reqBody:  `{"email": "[email protected]", "pwd": "invalidpassword"}`,
			wantCode: http.StatusBadRequest,
			wantBody: `{"msg": "密码必须大于8位,包含数字、特殊字符"}`,
		},
		{
			name:     "邮箱或密码不匹配",
			reqBody:  `{"email": "[email protected]", "pwd": "hello#world123"}`,
			wantCode: http.StatusBadRequest,
			wantBody: `{"msg": "邮箱或密码不匹配!"}`,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// 创建一个 gin 的上下文
			server := gin.Default()
			h := NewUserHandler()
			h.RegisterRoutes(server)
			// mock 创建一个 http 请求
			req, err := http.NewRequest(
				http.MethodPost,                     // 请求方法
				"/user/login",                       // 请求路径
				bytes.NewBuffer([]byte(tc.reqBody)), // 请求体
			)
			// 断言没有错误
			assert.NoError(t, err)
			// 设置请求头
			req.Header.Set("Content-Type", "application/json")
			// 创建一个响应
			resp := httptest.NewRecorder()
			// 服务端处理请求
			server.ServeHTTP(resp, req)
			// 断言响应码和响应体
			assert.Equal(t, tc.wantCode, resp.Code)
			// 断言 JSON 字符串是否相等
			assert.JSONEq(t, tc.wantBody, resp.Body.String())
		})
	}
}

在这个例子中,我们创建了一个简单的 HTTP 请求,TestUserHandler_Login 函数定义了一个测试函数,用于测试用户登录功能的不同情况。

  1. testCases 列表定义了多个测试用例,每个测试用例包含了测试名称、请求体、期望的 HTTP 状态码和期望的响应体内容。
  2. 使用 for 循环遍历测试用例列表,每次循环创建一个新的测试子函数,并在其中模拟 HTTP 请求发送给登录接口。
  3. 在每个测试子函数中,先创建一个 Gin 的默认上下文和用户处理器 UserHandler,然后注册路由并创建一个模拟的 HTTP 请求。
  4. 通过 httptest.NewRecorder() 创建一个响应记录器,使用 server.ServeHTTP(resp, req) 处理模拟请求,得到响应结果。
  5. 最后使用断言来验证实际响应的 HTTP 状态码和响应体是否与测试用例中的期望一致。

最后,使用Goland 运行测试,结果如下:

二、gock

2.1介绍

gock 可以帮助你在测试过程中模拟 HTTP 请求和响应,这对于测试涉及外部 API 调用的应用程序非常有用。它可以让你轻松地定义模拟请求,并验证你的应用程序是否正确处理了这些请求。

GitHub 地址:github.com/h2non/gock

2.2 安装

你可以通过以下方式安装 gock:

go get -u github.com/h2non/gock

导入 gock 包:

import "github.com/h2non/gock"

2.3 基本使用

gock 的基本用法如下:

  1. 启动拦截器:在测试开始前,使用 gock.New 函数启动拦截器,并指定你想要拦截的域名和端口。
  2. 定义拦截规则:你可以使用 gock.Intercept 方法来定义拦截规则,比如拦截特定的 URL、方法、头部信息等。
  3. 设置响应:你可以使用 gock.NewJsongock.NewText 等方法来设置拦截后的响应内容。
  4. 运行测试:在定义了拦截规则和响应后,你可以运行测试,gock 会拦截你的 HTTP 请求,并返回你设置的响应。

2.4 举个例子

2.4.1 前置代码

如果我们是在代码中请求外部API的场景(比如通过API调用其他服务获取返回值)又该怎么编写单元测试呢?

例如,我们有以下业务逻辑代码,依赖外部API:http://your-api.com/post提供的数据。

// ReqParam API请求参数
type ReqParam struct {
	X int `json:"x"`
}

// Result API返回结果
type Result struct {
	Value int `json:"value"`
}

func GetResultByAPI(x, y int) int {
	p := &ReqParam{X: x}
	b, _ := json.Marshal(p)

	// 调用其他服务的API
	resp, err := http.Post(
		"http://your-api.com/post",
		"application/json",
		bytes.NewBuffer(b),
	)
	if err != nil {
		return -1
	}
	body, _ := ioutil.ReadAll(resp.Body)
	var ret Result
	if err := json.Unmarshal(body, &ret); err != nil {
		return -1
	}
	// 这里是对API返回的数据做一些逻辑处理
	return ret.Value + y
}

在对类似上述这类业务代码编写单元测试的时候,如果不想在测试过程中真正去发送请求或者依赖的外部接口还没有开发完成时,我们可以在单元测试中对依赖的API进行mock。

2.4.2 测试用例

使用gock对外部API进行mock,即mock指定参数返回约定好的响应内容。 下面的代码中mock了两组数据,组成了两个测试用例。

package gock_demo

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/h2non/gock.v1"
)

func TestGetResultByAPI(t *testing.T) {
	defer gock.Off() // 测试执行后刷新挂起的mock

	// mock 请求外部api时传参x=1返回100
	gock.New("http://your-api.com").
		Post("/post").
		MatchType("json").
		JSON(map[string]int{"x": 1}).
		Reply(200).
		JSON(map[string]int{"value": 100})

	// 调用我们的业务函数
	res := GetResultByAPI(1, 1)
	// 校验返回结果是否符合预期
	assert.Equal(t, res, 101)

	// mock 请求外部api时传参x=2返回200
	gock.New("http://your-api.com").
		Post("/post").
		MatchType("json").
		JSON(map[string]int{"x": 2}).
		Reply(200).
		JSON(map[string]int{"value": 200})

	// 调用我们的业务函数
	res = GetResultByAPI(2, 2)
	// 校验返回结果是否符合预期
	assert.Equal(t, res, 202)

	assert.True(t, gock.IsDone()) // 断言mock被触发
}

标签:HTTP,请求,gock,单元测试,API,gin,msg,http,com
From: https://www.cnblogs.com/taoxiaoxin/p/18141253

相关文章

  • Spring API DOC未授权访问/SpringBoot Actuator 未授权访问测试方法
    插件安装需要先安装并配置java环境,然后在burpsuite中安装apikit插件 工具扫描1)在BurpSuite中抓取请求:你将在BurpSuite中的`Proxy`标签的`HTTPhistory`中看到所有通过代理发送的请求。2)使用APIkit进行测试:在BurpSuite中,发送想要测试的请求选择**DoAutoAPIscan**来发......
  • Got socket error trying to find package flutter_lints at https://pub.dev Flutter
    最近想继续玩下Flutter,发现pub.dev居然被封锁了,试了下网上的方案,都不可行,尝试组合了一下,用下面的方案解决了。第一步:找到这个文件D:\flutter\packages\flutter_tools\lib\src\http_host_validator.dart把下面的地址修改为:constStringkPubDev='https://pub-web.flutter-io......
  • http请求头中application/x-www-form-urlencoded和multipart/form-data区别
    application/x-www-form-urlencoded和multipart/form-data是两种不同的Content-Type,它们在网络请求中(尤其是POST请求)用来指定表单数据的编码格式application/x-www-form-urlencoded:•这是最常见的表单数据编码方式,也是HTML表单的默认编码类型。•所有表单字段名和值都会......
  • day14_我的Java学习笔记 (常用API、Lambda、常见算法)
    1.常用API1.1Date类【案例】:计算出当前时间往后走1小时121秒之后的时间是多少。1.2SimpleDateFormat【练习】:秒杀活动1.3Calendar2.JDK8新增日期类2.1概述、LocalTime/LocalDate/LocalDateTime2.2Ins......
  • Command line is too long. Shorten command line for JooLunMallApiApplication or a
      在启动微服务项目的某个时候,一直启动不起来,报Commandlineistoolong.ShortencommandlineforApplicationoralsoforApplicatio,在网上查阅资料后,发现这个问题,是因为启动命令太长(就是main方法类)。  解决方法:  选择工具栏的Run并下拉选Run,之后会有个弹窗,选择......
  • Go 单元测试基本介绍
    Go单元测试基本介绍一、单元测试基本介绍1.1什么是单元测试?单元测试(UnitTests,UT)是一个优秀项目不可或缺的一部分,是对软件中的最小可测试部分进行检查和验证。在面向对象编程中,最小测试单元通常是一个方法或函数。单元测试通常由开发者编写,用于验证代码的一个很小的、很......
  • 如何在Semantic Kernel中使用第三方代理OpenAI API接口
    最近手里没有官方的OpenAIAPIKey了,只能在第三方代理平台去购买APIKey。但是使用SemanticKernel的时候发现AddOpenAIChatCompletion不像AddAzureOpenAIChatCompletion那样可以选择终结点去看了下SemanticKernel项目下的issues,这个问题被提及了好几次,但是官方仍然没有提供End......
  • Docker-compose部署项目(Fastapi项目为例)
    1.有Dockerfile文件进行部署,Dockerfile内容参考FROM127.0.0.1:5000/py3.10-1COPYrequirements.txt.RUNpipinstall-ipip源-rrequirements.txt2.Docker-compose.yml文件env_file指定环境变量,看项目是否使用env环境变量volumes挂在工作目录映射,工作目录更改代码容......
  • 免费版ChatGPT API Key生成
    ChatGPT现在虽然都免费了,但是不岢雪(那俩字竟然是敏感字)上网还是很麻烦,网上有很多gpt网站可以用,但是自己用来开发的话,还是需要一个apikey的项目地址https://github.com/chatanywhere/GPT_API_free可以直接去官网看如何申请申请地址https://api.chatanywhere.org/v1/oauth/fr......
  • Docker使用Dockerfile部署Fastapi应用
    1#使用官方的Python运行时作为基础镜像2FROMpython:3.9-slim34#设置工作目录5WORKDIR/app67#复制应用程序的依赖文件到工作目录8COPYrequirements.txt.910#安装应用程序的依赖11RUNpipinstall--no-cache-dir-rrequirements.txt12......