网络测试

网络测试

如何使用httptest和gock工具进行网络测试。

而实际工作中的业务场景往往会比较复杂,无论我们的代码是作为server端对外提供服务或者还是我们依赖别人提供的网络服务(调用别人提供的API接口)的场景,我们通常都不想在测试过程中真正的建立网络连接。本文就专门介绍如何在上述两种场景下mock网络测试。源码地址

一、httptest

在Web开发场景下的单元测试,如果涉及到HTTP请求推荐大家使用Go标准库 net/http/httptest 进行测试,能够显著提高测试效率。

我们以常见的gin框架为例,演示如何为http server编写单元测试。

假设我们的业务逻辑是搭建一个http server端,对外提供HTTP服务。我们编写了一个helloHandler函数,用来处理用户请求。

package http_test_demo

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

/*
@author RandySun
@create 2022-04-30-21:06
*/

type Param struct {
	Name string `json:"name"`
}

//
//  helloHandler
//  @Description: hello请求处理函数
//  @param c
//
func helloHandler(c *gin.Context) {
	var p Param
	if err := c.ShouldBindJSON(&p); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"msg": "we need a name",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"msg": fmt.Sprintf("hello %s", p.Name),
	})
}

//
// SetupRoute
//  @Description: 路由
//  @return *gin.Engine
//
func SetupRoute() *gin.Engine {
	router := gin.Default()
	router.POST("/hello", helloHandler)
	return router
}

现在我们需要为helloHandler函数编写单元测试,这种情况下我们就可以使用httptest这个工具mock一个HTTP请求和响应记录器,让我们的server端接收并处理我们mock的HTTP请求,同时使用响应记录器来记录server端返回的响应内容。

生成单元测试文件:

http_test_demo> gotests -all -w .\gin.go
Generated Test_helloHandler
Generated TestSetupRoute

单元测试的示例代码如下:

package http_test_demo

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/assert"
)

func Test_helloHandler(t *testing.T) {
	type args struct {
		c *gin.Context
	}
	// 创建测试用例
	tests := []struct {
		name   string
		param  string
		expect string
	}{
		{"base case1", `{"name": "RandySun"}`, "hello RandySun"},
		{"bad case2", "", "we need a name"},
		{"bad case3", "", "name"},
	}

	// 注册路由
	r := SetupRouter()

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// mock一个HTTP请求
			req := httptest.NewRequest(
				"POST",                      // 请求方法
				"/hello",                    // 请求URL
				strings.NewReader(tt.param), // 请求参数
			)

			// mock一个响应记录器
			w := httptest.NewRecorder()

			// 让server端处理mock请求并记录返回的响应内容
			r.ServeHTTP(w, req)

			// 校验状态码是否符合预期
			assert.Equal(t, http.StatusOK, w.Code)

			// 解析并检验响应内容是否复合预期
			var resp map[string]string
			err := json.Unmarshal([]byte(w.Body.String()), &resp)
			t.Logf("response msg: %#v", resp)
			assert.Nil(t, err)
			assert.Equal(t, tt.expect, resp["msg"])

		})
	}
}

执行单元测试,查看测试结果。

http_test_demo> go test -v
=== RUN   Test_helloHandler
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /hello                    --> golang-unit-test-example/02http_test_demo.helloHandler (3 handlers)
=== RUN   Test_helloHandler/base_case
[GIN] 2022/04/30 - 21:22:31 |?[97;42m 200 ?[0m|            0s |       192.0.2.1 |?[97;46m POST    ?[0m "/hello"
=== RUN   Test_helloHandler/bad_case
[GIN] 2022/04/30 - 21:22:32 |?[97;42m 200 ?[0m|            0s |       192.0.2.1 |?[97;46m POST    ?[0m "/hello"
--- PASS: Test_helloHandler (0.02s)
    --- PASS: Test_helloHandler/base_case (0.02s)
    --- PASS: Test_helloHandler/bad_case (0.00s)
PASS
ok      golang-unit-test-example/02http_test_demo       0.225s

通过这个示例我们就掌握了如何使用httptest在HTTP Server服务中为请求处理函数编写单元测试了。

image-20220430215156082

二、gock

上面的示例介绍了如何在HTTP Server服务类场景下为请求处理函数编写单元测试,那么如果我们是在代码中请求外部API的场景(比如通过API调用其他服务获取返回值)又该怎么编写单元测试呢?

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

package gock_demo

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
)

/*
@author RandySun
@create 2022-04-30-22:06
*/

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

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

//
// GetResultByApi
//  @Description: 调用外部api
//  @param x
//  @param y
//  @return int
//
func GetResultByAPI(x, y int) int {
	p := &ReqParam{
		X: x,
	}
	b, _ := json.Marshal(p)
	// 调用其它服务的API
	response, err := http.Post(
		"http://your-api.com/post",
		"application/json",
		bytes.NewBuffer(b),
	)
	if err != nil {
		return -1
	}
	body, _ := ioutil.ReadAll(response.Body)
	var rest Result
	if err != json.Unmarshal(https://img2022.cnblogs.com/blog/1739642/202205/1739642-20220503150435735-2070165654.png 'body, &rest') {
		return -1
	}
	fmt.Println(rest.Value, "8888")
	// 这里是对API返回的数据做一些逻辑处理
	return rest.Value + y
}

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

这里推荐使用gock这个库。

三、安装

go get -u gopkg.in/h2non/gock.v1

官网示例

四、使用示例

生成测试文件:

gock_demo> gotests -all -w .\api.go
Generated TestGetResultByApi

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

// api_test.go
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被触发
}

执行上面写好的单元测试,看一下测试结果。

gock_demo> go test -v
=== RUN   TestGetResultByApi
--- PASS: TestGetResultByApi (0.00s)
PASS
ok      golang-unit-test-example/03gock_demo    0.188s

image-20220430224700800

测试结果和预期的完全一致。

在这个示例中,为了让大家能够清晰的了解gock的使用,我特意没有使用表格驱动测试。给大家留一个小作业:自己动手把这个单元测试改写成表格驱动测试的风格,就当做是对最近两篇教程的复习和测验。

五、总结

在日常工作开发中为代码编写单元测试时如何处理外部依赖是最常见的问题,本文介绍了如何使用httptestgock工具mock相关依赖。

posted @ 2022-05-03 15:23  RandySun  阅读(574)  评论(0编辑  收藏  举报