Go:單元測試之模擬HTTP調用


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開始使用標準方法來模擬調用。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末篮幢,一起剝皮案震驚了整個濱河市大刊,隨后出現的幾起案子,更是在濱河造成了極大的恐慌三椿,老刑警劉巖缺菌,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異搜锰,居然都是意外死亡伴郁,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門纽乱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛾绎,“玉大人,你說我怎么就攤上這事鸦列∽夤冢” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵薯嗤,是天一觀的道長顽爹。 經常有香客問我,道長骆姐,這世上最難降的妖魔是什么镜粤? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任捏题,我火速辦了婚禮,結果婚禮上肉渴,老公的妹妹穿的比我還像新娘公荧。我一直安慰自己,他們只是感情好同规,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布循狰。 她就那樣靜靜地躺著,像睡著了一般券勺。 火紅的嫁衣襯著肌膚如雪绪钥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天关炼,我揣著相機與錄音程腹,去河邊找鬼。 笑死儒拂,一個胖子當著我的面吹牛寸潦,可吹牛的內容都是我干的。 我是一名探鬼主播侣灶,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼甸祭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了褥影?” 一聲冷哼從身側響起池户,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凡怎,沒想到半個月后校焦,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡统倒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年寨典,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片房匆。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡耸成,死狀恐怖,靈堂內的尸體忽然破棺而出浴鸿,到底是詐尸還是另有隱情井氢,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布岳链,位于F島的核電站花竞,受9級特大地震影響,放射性物質發(fā)生泄漏掸哑。R本人自食惡果不足惜约急,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一零远、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厌蔽,春花似錦牵辣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拐云,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間近她,已是汗流浹背叉瘩。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留粘捎,地道東北人薇缅。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像攒磨,于是被迫代替她去往敵國和親泳桦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容