美文网首页
Go:单元测试之模拟HTTP调用

Go:单元测试之模拟HTTP调用

作者: Go语言由浅入深 | 来源:发表于2021-12-31 07:52 被阅读0次

Go开发中单元测试是写代码的一个必备环节,它可以保证你写的代码接口逻辑符合预期。但是很多时候,在写单测时需要使用有一些外部资源,最常见的包括数据库调用、http调用或者rpc调用等等。

解决单测中调用外部资源的一个常见方法就是使用Mock技术,也就是所谓的模拟一个外部资源调用过程。其实模拟的本质就是面向对象思想中的接口实现,在Go语言中要调用一个外部资源,可以自定义一个结构体,然后该结构体实现外部资源所包含的接口方法,就能通过调用自定义的结构体方法来模拟实际外部资源调用。

今天我们以HTTP调用为例来说明mock的使用。因为http的调用涉及客户端和服务端两个方面,因此我们要模拟的话可以选择模拟客户端,也可以选择模拟服务的。

选择模拟客户端的话,需要更改API实现以使用HTTPClient接口。从长远来看,这是一个相当大的问题,因为你不知道在下一个版本的Golang代码库中,HTTP客户端会有什么变更。如果mock HTTP客户端,就会遇到这个问题。所以,在本文中,我们将使用内置的测试库模拟HTTP服务器。不需要创建自己的接口,因为它都是由Golang标准库httptest提供的。

目录结构

$ go mod init example
go: creating new go.mod: module example
$ mkdir -p external
$ touch external/{external.go,external_test.go}
$ tree .
.
├── external
│   ├── external.go
│   └── external_test.go
└── go.mod
1 directory, 3 files

代码实现

package external

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    "time"
)

var ErrResponseNotOk = errors.New("response not ok")

type (
    //待测试接口返回数据类型定义
    Data struct {
        ID   string `json:"id"`
        Name string `json:"name"`
    }

    //该接口FetchData方法需要调用外部http服务
    External interface {
        FetchData(ctx context.Context, id string) (*Data, error)
    }

    //定义结构体用户http调用
    v1 struct {
        baseURL string
        client  *http.Client
        timeout time.Duration
    }
)

//创建结构体初始化
func New(baseURL string, client *http.Client, timeout time.Duration) *v1 {
    return &v1{
        baseURL: baseURL,
        client:  client,
        timeout: timeout,
    }
}

//FetchData是需要写单测的接口方法,内部包含调用外部http服务
func (v *v1) FetchData(ctx context.Context, id string) (*Data, error) {
    url := fmt.Sprintf("%s/?id=%s", v.baseURL, id)

    ctx, cancel := context.WithTimeout(ctx, v.timeout)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }

    resp, err := v.client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("%w. %s", ErrResponseNotOk,
            http.StatusText(resp.StatusCode))
    }
    var d *Data
    return d, json.NewDecoder(resp.Body).Decode(&d)
}

上面的External接口就是需要写的单元测试接口,其中FetchData方法内部包含调用外部http服务,因此需要我们实现http调用的模拟过程。

让我们来完成要模拟的External接口。

package external_test

import (
    "example/external"
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
    "time"
)

var (
    server *httptest.Server
    ext external.External
)

func TestMain(m *testing.M)  {
    fmt.Println("mocking server")
        //模拟http服务端,httptest.NewServer返回一个实现http.Server接口的实例
    server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,
        r *http.Request) {
        //这里是模拟服务端接口处理程序
    }))
    
    fmt.Println("mocking external")
    ext = external.New(server.URL, http.DefaultClient, time.Second)
    
    fmt.Println("run tests")
    m.Run()
}

首先需要模拟HTTP服务端以及External对象。如代码25行所示:

...
ext = external.New(server.URL, http.DefaultClient, time.Second)
...

可以使用server.URL作为baseURL,因此HTTP调用baseURL时将路由到httptest.Server中。也就是我们模拟的HTTP服务端,而不是实际的HTTP调用。

创建了模拟的http服务端之后,需要实现模拟服务端的接口处理函数,如下所示:

//模拟服务的响应
func mockFetchDataEndPoint(w http.ResponseWriter, r *http.Request)  {
    ids, ok := r.URL.Query()["id"]
    
    sc := http.StatusOK
    m := make(map[string]interface{})
    
    if !ok || len(ids[0]) == 0 {
        sc = http.StatusBadRequest
    } else {
        m["id"] = "mock"      //FetchData接口的返回值Data.ID
        m["name"] = "mock"    ////FetchData接口的返回值Data.Name
    }
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(sc)
    //模拟返回值响应
    json.NewEncoder(w).Encode(m)
}

然后,将接口处理程序放在http模拟服务端中并添加路由。

...
func TestMain(m *testing.M)  {
    fmt.Println("mocking server")
    server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter,
        r *http.Request) {
        switch strings.TrimSpace(r.URL.Path) {
        case "/":
            mockFetchDataEndPoint(w, r)
        default:
            http.NotFoundHandler().ServeHTTP(w, r)
        }
    }))
...

创建单元测试

...
func fatal(t *testing.T, want, got interface{}) {
    t.Helper()
    t.Fatalf(`want: %v, got: %v`, want, got)
}
func TestExternal_FetchData(t *testing.T) {
    tt := []struct {
        name     string
        id       string
        wantData *external.Data
        wantErr  error
    }{
        {
            name:     "response not ok",
            id:       "",
            wantData: nil,
            wantErr:  external.ErrResponseNotOk,
        },
        {
            name: "data found",
            id:   "mock",
            wantData: &external.Data{
                ID:   "mock",
                Name: "mock",
            },
            wantErr: nil,
        },
    }
    for i := range tt {
        tc := tt[i]
        t.Run(tc.name, func(t *testing.T) {
            t.Parallel()
            gotData, gotErr := ext.FetchData(context.Background(), tc.id)
            if !errors.Is(gotErr, tc.wantErr) {
                fatal(t, tc.wantErr, gotErr)
            }
            if !reflect.DeepEqual(gotData, tc.wantData) {
                fatal(t, tc.wantData, gotData)
            }
        })
    }
}

现在您已经模拟了HTTP服务器,单元测试本身并没有什么特别之处。您可以和平常一样开始编写单元测试。

总结:

通过实现http模拟服务端,您已经大大改进了单元测试。与模拟HTTP客户端相比,模拟HTTP服务器更具可读性也更合适。您不需要创建HTTP客户端的接口,并通过使用httptest开始使用标准方法来模拟调用。

相关文章

  • Go:单元测试之模拟HTTP调用

    Go开发中单元测试是写代码的一个必备环节,它可以保证你写的代码接口逻辑符合预期。但是很多时候,在写单测时需要使用有...

  • 【单元测试】- 模拟HTTP请求调用controller

    MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller调用,这样使得测试速度...

  • Go 中模拟 Kubernetes 客户端进行单元测试

    Go 中模拟 Kubernetes 客户端进行单元测试 是的,我们可以模仿 K8s Client! 编写单元测试一...

  • Go RPC demo

    模拟RPC调用 server.go client.go client2.go 多参数把多参数封装入结构体中 add...

  • 16.手撕Go语言-测试

    Go提供了test工具用于代码的单元测试,test工具会查找包下以_test.go结尾的文件,调用测试文件中以Te...

  • SpringBoot单元测试

    1、Spring Boot的单元测试 依赖Maven配置 示例代码 2、MockMvc模拟Http请求测试 使用说...

  • Spring Boot 单元测试

    本文介绍 Spring Boot 2 单元测试实现方案。 目录 开发环境 测试 HTTP 接口 Mock 调用对象...

  • go 文件服务器问题

    在调用go语言编写的server进行下载文件时出现 原因:go server响应时的逻辑是 而go http 的底...

  • go 单元测试

    单元测试 Go 语言测试框架可以让我们很容易地进行单元测试,但是需要遵循五点规则: 含有单元测试代码的 go 文件...

  • JUnit之TestCase和TestSuite详解

    Android Studio下单元测试的本质其实是根据通过书写JAVA测试代码,通过模拟用户调用相应的方法,或者使...

网友评论

      本文标题:Go:单元测试之模拟HTTP调用

      本文链接:https://www.haomeiwen.com/subject/mvxgqrtx.html