golang轻松模拟外部HTTP响应插件库httpmock的使用
Golang轻松模拟外部HTTP响应插件库httpmock的使用
httpmock是一个用于轻松模拟外部HTTP响应的Golang库,特别适合在单元测试中模拟HTTP请求。
安装
支持Go 1.16到1.24版本,使用v1分支而非master分支。
在代码中导入:
import "github.com/jarcoal/httpmock"
执行go mod tidy
或go test
会自动将最新版本的httpmock添加到你的go.mod文件中。
使用示例
简单示例
func TestFetchArticles(t *testing.T) {
httpmock.Activate(t)
// 精确URL匹配
httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles",
httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`))
// 正则表达式匹配(也可以使用httpmock.RegisterRegexpResponder)
httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/\d+\z`,
httpmock.NewStringResponder(200, `{"id": 1, "name": "My Great Article"}`))
// 执行会发起请求到articles的代码
...
// 获取总调用次数
httpmock.GetTotalCallCount()
// 获取已注册响应器的调用次数
info := httpmock.GetCallCountInfo()
info["GET https://api.mybiz.com/articles"] // 对https://api.mybiz.com/articles的GET调用次数
info["GET https://api.mybiz.com/articles/id/12"] // 对https://api.mybiz.com/articles/id/12的GET调用次数
info[`GET =~^https://api\.mybiz\.com/articles/id/\d+\z`] // 对https://api.mybiz.com/articles/id/<任意数字>的GET调用次数
}
高级示例
func TestFetchArticles(t *testing.T) {
httpmock.Activate(t)
// 我们的文章数据库
articles := make([]map[string]interface{}, 0)
// 模拟列出文章
httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles",
func(req *http.Request) (*http.Response, error) {
resp, err := httpmock.NewJsonResponse(200, articles)
if err != nil {
return httpmock.NewStringResponse(500, ""), nil
}
return resp, nil
})
// 通过正则表达式子匹配(\d+)返回与请求相关的文章
httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/(\d+)\z`,
func(req *http.Request) (*http.Response, error) {
// 从请求中获取ID
id := httpmock.MustGetSubmatchAsUint(req, 1) // 1=第一个正则子匹配
return httpmock.NewJsonResponse(200, map[string]interface{}{
"id": id,
"name": "My Great Article",
})
})
// 模拟添加新文章
httpmock.RegisterResponder("POST", "https://api.mybiz.com/articles",
func(req *http.Request) (*http.Response, error) {
article := make(map[string]interface{})
if err := json.NewDecoder(req.Body).Decode(&article); err != nil {
return httpmock.NewStringResponse(400, ""), nil
}
articles = append(articles, article)
resp, err := httpmock.NewJsonResponse(200, article)
if err != nil {
return httpmock.NewStringResponse(500, ""), nil
}
return resp, nil
})
// 模拟添加特定文章,当请求体包含`"type":"toy"`时返回Bad Request响应
httpmock.RegisterMatcherResponder("POST", "https://api.mybiz.com/articles",
httpmock.BodyContainsString(`"type":"toy"`),
httpmock.NewStringResponder(400, `{"reason":"Invalid article type"}`))
// 执行添加和检查文章的代码
}
匹配算法
当捕获到GET http://example.tld/some/path?b=12&a=foo&a=bar
请求时,所有标准响应器会按以下顺序检查URL或路径,第一个匹配停止搜索:
http://example.tld/some/path?b=12&a=foo&a=bar
(原始URL)http://example.tld/some/path?a=bar&a=foo&b=12
(排序后的查询参数)http://example.tld/some/path
(不带查询参数)/some/path?b=12&a=foo&a=bar
(不带协议和主机的原始URL)/some/path?a=bar&a=foo&b=12
(同上,但查询参数排序)/some/path
(仅路径)
如果没有标准响应器匹配,则检查正则表达式响应器,顺序相同,第一个匹配停止搜索。
测试框架集成示例
与go-testdeep和tdsuite集成
// article_test.go
import (
"testing"
"github.com/jarcoal/httpmock"
"github.com/maxatome/go-testdeep/helpers/tdsuite"
"github.com/maxatome/go-testdeep/td"
)
type MySuite struct{}
func (s *MySuite) Setup(t *td.T) error {
// 拦截所有HTTP请求
httpmock.Activate(t)
return nil
}
func (s *MySuite) PostTest(t *td.T, testName string) error {
// 每次测试后移除所有mock
httpmock.Reset()
return nil
}
func TestMySuite(t *testing.T) {
tdsuite.Run(t, &MySuite{})
}
func (s *MySuite) TestArticles(assert, require *td.T) {
httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles.json",
httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`))
// 执行会发起请求到articles.json的代码
}
与Ginkgo集成
// article_suite_test.go
import (
// ...
"github.com/jarcoal/httpmock"
)
// ...
var _ = BeforeSuite(func() {
// 拦截所有HTTP请求
httpmock.Activate()
})
var _ = BeforeEach(func() {
// 移除所有mock
httpmock.Reset()
})
var _ = AfterSuite(func() {
httpmock.DeactivateAndReset()
})
// article_test.go
import (
// ...
"github.com/jarcoal/httpmock"
)
var _ = Describe("Articles", func() {
It("returns a list of articles", func() {
httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles.json",
httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`))
// 执行会发起请求到articles.json的代码
})
})
与Ginkgo和Resty集成
// article_suite_test.go
import (
// ...
"github.com/jarcoal/httpmock"
"github.com/go-resty/resty/v2"
)
// ...
// 全局client(使用resty.New()每次会创建新的transport,
// 所以需要在这里和发起请求时使用同一个)
var client = resty.New()
var _ = BeforeSuite(func() {
// 拦截所有HTTP请求
httpmock.ActivateNonDefault(client.GetClient())
})
var _ = BeforeEach(func() {
// 移除所有mock
httpmock.Reset()
})
var _ = AfterSuite(func() {
httpmock.DeactivateAndReset()
})
// article_test.go
import (
// ...
"github.com/jarcoal/httpmock"
)
type Article struct {
Status struct {
Message string `json:"message"`
Code int `json:"code"`
} `json:"status"`
}
var _ = Describe("Articles", func() {
It("returns a list of articles", func() {
fixture := `{"status":{"message": "Your message", "code": 200}}`
// 必须使用NewJsonResponder来获取application/json content-type
// 或者创建一个go对象而不是使用json.RawMessage
responder, _ := httpmock.NewJsonResponder(200, json.RawMessage(`{"status":{"message": "Your message", "code": 200}}`)
fakeUrl := "https://api.mybiz.com/articles.json"
httpmock.RegisterResponder("GET", fakeUrl, responder)
// 将文章获取到结构体中
articleObject := &Article{}
_, err := resty.R().SetResult(articleObject).Get(fakeUrl)
// 对articleObject执行操作...
})
})
更多关于golang轻松模拟外部HTTP响应插件库httpmock的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于golang轻松模拟外部HTTP响应插件库httpmock的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
使用httpmock库轻松模拟外部HTTP响应
httpmock是Go语言中一个非常实用的HTTP模拟库,可以让你在单元测试中轻松模拟外部HTTP服务的响应,而不需要实际发起网络请求。下面我将详细介绍如何使用httpmock。
安装httpmock
首先安装httpmock库:
go get gopkg.in/jarcoal/httpmock.v1
基本使用方法
1. 简单的GET请求模拟
package main
import (
"fmt"
"net/http"
"testing"
"gopkg.in/jarcoal/httpmock.v1"
)
func TestGetUser(t *testing.T) {
// 激活httpmock
httpmock.Activate()
defer httpmock.DeactivateAndReset()
// 模拟GET请求
httpmock.RegisterResponder("GET", "https://api.example.com/users/1",
httpmock.NewStringResponder(200, `{"id": 1, "name": "John Doe"}`))
// 发起请求
resp, err := http.Get("https://api.example.com/users/1")
if err != nil {
t.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
// 验证响应
if resp.StatusCode != 200 {
t.Errorf("期望状态码200,得到%d", resp.StatusCode)
}
// 这里可以继续验证响应体内容...
fmt.Println("模拟响应成功!")
}
2. 带参数的请求模拟
func TestGetUserWithQuery(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
// 模拟带查询参数的请求
httpmock.RegisterResponder("GET", "https://api.example.com/users",
func(req *http.Request) (*http.Response, error) {
// 验证查询参数
if req.URL.Query().Get("active") != "true" {
return httpmock.NewStringResponse(400, "缺少active参数"), nil
}
return httpmock.NewStringResponse(200, `[{"id":1,"active":true}]`), nil
})
// 发起带参数的请求
resp, err := http.Get("https://api.example.com/users?active=true")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// 验证响应
if resp.StatusCode != 200 {
t.Errorf("期望状态码200,得到%d", resp.StatusCode)
}
}
3. POST请求模拟
func TestCreateUser(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
// 模拟POST请求
httpmock.RegisterResponder("POST", "https://api.example.com/users",
func(req *http.Request) (*http.Response, error) {
// 这里可以验证请求体
return httpmock.NewJsonResponse(201, map[string]interface{}{
"id": 123,
"name": "New User",
})
})
// 发起POST请求
resp, err := http.Post("https://api.example.com/users", "application/json", strings.NewReader(`{"name":"New User"}`))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// 验证响应
if resp.StatusCode != 201 {
t.Errorf("期望状态码201,得到%d", resp.StatusCode)
}
}
高级功能
1. 模拟延迟响应
func TestDelayedResponse(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
// 模拟延迟500毫秒的响应
httpmock.RegisterResponder("GET", "https://api.example.com/slow",
httpmock.NewStringResponder(200, `{"status":"ok"}`).Delay(500*time.Millisecond))
start := time.Now()
resp, err := http.Get("https://api.example.com/slow")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
elapsed := time.Since(start)
if elapsed < 500*time.Millisecond {
t.Errorf("响应应该延迟至少500ms,实际延迟: %v", elapsed)
}
}
2. 验证请求次数
func TestCallCount(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
// 注册模拟响应
httpmock.RegisterResponder("GET", "https://api.example.com/users",
httpmock.NewStringResponder(200, `[]`))
// 第一次调用
_, _ = http.Get("https://api.example.com/users")
// 第二次调用
_, _ = http.Get("https://api.example.com/users")
// 验证调用次数
info := httpmock.GetCallCountInfo()
if info["GET https://api.example.com/users"] != 2 {
t.Errorf("期望调用2次,实际调用%d次", info["GET https://api.example.com/users"])
}
}
3. 使用JSON响应
func TestJsonResponse(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
// 注册JSON响应
httpmock.RegisterResponder("GET", "https://api.example.com/products/1",
httpmock.NewJsonResponderOrPanic(200, map[string]interface{}{
"id": 1,
"name": "Laptop",
"price": 999.99,
}))
resp, err := http.Get("https://api.example.com/products/1")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// 解析JSON响应
var product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
if err := json.NewDecoder(resp.Body).Decode(&product); err != nil {
t.Fatal(err)
}
if product.Name != "Laptop" {
t.Errorf("期望产品名Laptop,得到%s", product.Name)
}
}
最佳实践
-
每个测试用例后重置模拟器:使用
defer httpmock.DeactivateAndReset()
确保每个测试用例都有干净的模拟环境。 -
验证请求细节:在Responder函数中验证请求头、请求体等细节。
-
组织模拟响应:对于复杂的API,可以创建辅助函数来生成模拟响应。
-
结合测试框架:可以与Go的标准testing包或其他测试框架(如testify)一起使用。
httpmock是Go语言测试中模拟HTTP请求的强大工具,它能帮助你编写不依赖外部服务的可靠单元测试,同时保持测试的快速执行。