golang轻松模拟外部HTTP响应插件库httpmock的使用

Golang轻松模拟外部HTTP响应插件库httpmock的使用

httpmock是一个用于轻松模拟外部HTTP响应的Golang库,特别适合在单元测试中模拟HTTP请求。

安装

支持Go 1.16到1.24版本,使用v1分支而非master分支。

在代码中导入:

import "github.com/jarcoal/httpmock"

执行go mod tidygo 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或路径,第一个匹配停止搜索:

  1. http://example.tld/some/path?b=12&a=foo&a=bar (原始URL)
  2. http://example.tld/some/path?a=bar&a=foo&b=12 (排序后的查询参数)
  3. http://example.tld/some/path (不带查询参数)
  4. /some/path?b=12&a=foo&a=bar (不带协议和主机的原始URL)
  5. /some/path?a=bar&a=foo&b=12 (同上,但查询参数排序)
  6. /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

1 回复

更多关于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)
	}
}

最佳实践

  1. 每个测试用例后重置模拟器:使用defer httpmock.DeactivateAndReset()确保每个测试用例都有干净的模拟环境。

  2. 验证请求细节:在Responder函数中验证请求头、请求体等细节。

  3. 组织模拟响应:对于复杂的API,可以创建辅助函数来生成模拟响应。

  4. 结合测试框架:可以与Go的标准testing包或其他测试框架(如testify)一起使用。

httpmock是Go语言测试中模拟HTTP请求的强大工具,它能帮助你编写不依赖外部服务的可靠单元测试,同时保持测试的快速执行。

回到顶部