与 httpmock 集成

概述

httpmock 经常用于 HTTP 相关的单元测试: 当你写完一个涉及到了远程 HTTP API 调用的功能,希望单元测试也能覆盖这部分逻辑,但如果在单元测试中对远程发起 HTTP 调用,在网络环境不通,或远程服务端 API 不可用时,单元测试就无法通过,此时我们可以使用 httpmock 来拦截我们的请求并直接返回我们所期望返回的数据,这样就可以在不经过网络传输的情况下直接完成 HTTP 相关的单元测试。

本文将介绍使用 req 开发的功能如何与 httpmock 集成来完成单元测试。

原理介绍

httpmock 主要是通过替换 http.ClientTransport 来实现请求拦截,让发起的 Request 不经过网络而直接根据 httpmock 的配置返回相应的 ResponsereqClient 内部也是用的 http.Client 来发起请求,通过 client.GetClient() 可以获取到内部的 http.Client,我们将其传给 httpmock,让它去替换掉 req 默认的 Transport 就可以实现与 httpmock 集成来轻松开发单元测试代码。

req 的 dump 能力与部分 debug 日志能力来自其自身实现的 Transport,httpmock 替换 Transport 后会丢失这部分的能力。

简单示例

假如我们开发了一个函数,用于获取指定 GitHub 账号的用户名:

import (
	"fmt"
	"github.com/imroc/req/v3"
)

func GetUserName(client *req.Client, loginName string) (name string, err error) {
	var user struct {
		Name string `json:"name"`
	}
	resp, err := client.R().
		SetSuccessResult(&user).
		SetPathParam("loginName", loginName).
		Get("https://api.github.com/users/{loginName}")
	if err != nil {
		return
	}

	if !resp.IsSuccessState() {
		err = fmt.Errorf("bad status code %q, body:%s", resp.StatusCode, resp.String())
		return
	}
	name = user.Name
	return
}

我们来利用 httpmock 写个单元测试:

import (
  "github.com/imroc/req/v3"
  "github.com/jarcoal/httpmock"
  "net/http"
  "testing"
)

func TestGetUserName(t *testing.T) {
  client := req.C()
  httpmock.ActivateNonDefault(client.GetClient())
  httpmock.RegisterResponder("GET", "https://api.github.com/users/imroc", func(request *http.Request) (*http.Response, error) {
    respBody := `{"login":"imroc","id":7448852,"node_id":"MDQ6VXNlcjc0NDg4NTI=","avatar_url":"https://avatars.githubusercontent.com/u/7448852?v=4","gravatar_id":"","url":"https://api.github.com/users/imroc","html_url":"https://github.com/imroc","followers_url":"https://api.github.com/users/imroc/followers","following_url":"https://api.github.com/users/imroc/following{/other_user}","gists_url":"https://api.github.com/users/imroc/gists{/gist_id}","starred_url":"https://api.github.com/users/imroc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/imroc/subscriptions","organizations_url":"https://api.github.com/users/imroc/orgs","repos_url":"https://api.github.com/users/imroc/repos","events_url":"https://api.github.com/users/imroc/events{/privacy}","received_events_url":"https://api.github.com/users/imroc/received_events","type":"User","site_admin":false,"name":"roc","company":"Tencent","blog":"https://imroc.cc","location":"China","email":null,"hireable":true,"bio":"I'm roc","twitter_username":"imrocchan","public_repos":137,"public_gists":0,"followers":407,"following":155,"created_at":"2014-04-30T10:50:46Z","updated_at":"2022-05-03T12:12:52Z"}`
    resp := httpmock.NewStringResponse(http.StatusOK, respBody)
    resp.Header.Set("Content-Type", "application/json; charset=utf-8")
    return resp, nil
  })
  name, err := GetUserName(client, "imroc")
  if err != nil {
    t.Error(err)
  }
  expectedName := "roc"
  if name != expectedName {
    t.Errorf("bad name result, expected %q, got %q", expectedName, name)
  }
}

给 SDK 写单元测试

使用 req 快速封装 SDK 中的 GitHub SDK 为例,我们来为它的 GetUserProfile 方法写个单元测试:

import (
	"github.com/imroc/req/v3"
	"github.com/jarcoal/httpmock"
	"net/http"
	"testing"
)

func TestClient_GetUserProfile(t *testing.T) {
	github := NewClient()
	httpmock.ActivateNonDefault(github.GetClient())
	httpmock.RegisterResponder("GET", "https://api.github.com/users/imroc", func(request *http.Request) (*http.Response, error) {
		respBody := `{"login":"imroc","id":7448852,"node_id":"MDQ6VXNlcjc0NDg4NTI=","avatar_url":"https://avatars.githubusercontent.com/u/7448852?v=4","gravatar_id":"","url":"https://api.github.com/users/imroc","html_url":"https://github.com/imroc","followers_url":"https://api.github.com/users/imroc/followers","following_url":"https://api.github.com/users/imroc/following{/other_user}","gists_url":"https://api.github.com/users/imroc/gists{/gist_id}","starred_url":"https://api.github.com/users/imroc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/imroc/subscriptions","organizations_url":"https://api.github.com/users/imroc/orgs","repos_url":"https://api.github.com/users/imroc/repos","events_url":"https://api.github.com/users/imroc/events{/privacy}","received_events_url":"https://api.github.com/users/imroc/received_events","type":"User","site_admin":false,"name":"roc","company":"Tencent","blog":"https://imroc.cc","location":"China","email":null,"hireable":true,"bio":"I'm roc","twitter_username":"imrocchan","public_repos":137,"public_gists":0,"followers":407,"following":155,"created_at":"2014-04-30T10:50:46Z","updated_at":"2022-05-03T12:12:52Z"}`
		resp := httpmock.NewStringResponse(http.StatusOK, respBody)
		resp.Header.Set("Content-Type", "application/json; charset=utf-8")
		return resp, nil
	})
	user, err := github.GetUserProfile("imroc")
	if err != nil {
		t.Error(err)
	}
	expectedName := "roc"
	if user.Name != expectedName {
		t.Errorf("bad name result, expected %q, got %q", expectedName, user.Name)
	}
}