Go開發(fā)中單元測試是寫代碼的一個必備環(huán)節(jié),它可以保證你寫的代碼接口邏輯符合預期阵漏。但是很多時候,在寫單測時需要使用有一些外部資源翻具,最常見的包括數據庫調用履怯、http調用或者rpc調用等等。
解決單測中調用外部資源的一個常見方法就是使用Mock技術呛占,也就是所謂的模擬一個外部資源調用過程虑乖。其實模擬的本質就是面向對象思想中的接口實現,在Go語言中要調用一個外部資源晾虑,可以自定義一個結構體疹味,然后該結構體實現外部資源所包含的接口方法,就能通過調用自定義的結構體方法來模擬實際外部資源調用帜篇。
今天我們以HTTP調用為例來說明mock的使用糙捺。因為http的調用涉及客戶端和服務端兩個方面,因此我們要模擬的話可以選擇模擬客戶端笙隙,也可以選擇模擬服務的洪灯。
選擇模擬客戶端的話,需要更改API實現以使用HTTPClient接口竟痰。從長遠來看签钩,這是一個相當大的問題,因為你不知道在下一個版本的Golang代碼庫中坏快,HTTP客戶端會有什么變更铅檩。如果mock HTTP客戶端,就會遇到這個問題莽鸿。所以昧旨,在本文中,我們將使用內置的測試庫模擬HTTP服務器祥得。不需要創(chuàng)建自己的接口兔沃,因為它都是由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
}
)
//創(chuàng)建結構體初始化
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調用暇咆。
創(chuàng)建了模擬的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)
}
}))
...
創(chuàng)建單元測試
...
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服務器更具可讀性也更合適逼肯。您不需要創(chuàng)建HTTP客戶端的接口耸黑,并通過使用httptest開始使用標準方法來模擬調用。