Go 如何模擬 HTTP 請求
這一篇,我們來學習一下如何使用 httpmock 庫,當使用 Go 語言編寫 HTTP 服務時,可以使用第三方庫來模擬 HTTP 請求和響應,例如:httpmock 庫。這個庫提供了一些方便的函數和結構體,用於在測試中模擬 HTTP 請求和驗證期望的 HTTP 調用。
輕鬆模擬來自外部資源的 http 響應。
https://github.com/jarcoal/httpmock
01 服務端
我們先使用 net/http 構建幾個接口,方便我們來模擬。
package main
import (
"context"
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", index)
mux.Handle("/foo", http.HandlerFunc(foo))
mux.Handle("/bar", http.HandlerFunc(bar))
server := http.Server{
Addr: ":8081",
Handler: mux,
}
server.ListenAndServe()
}
func index(w http.ResponseWriter, r *http.Request) {
fmt.Println("index")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"name": "index"}`))
}
func foo(w http.ResponseWriter, r *http.Request) {
fmt.Println("foo")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"name": "foo"}`))
}
func bar(w http.ResponseWriter, r *http.Request) {
fmt.Println("bar")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"name": "bar"}`))
}
運行完上面代碼,我們實現了 3 個接口,分別是 /,/foo,/bar !
02 httpmock 簡單示例
針對上面的接口,如果我們想用代碼去訪問 http 請求,可以是這樣子的:
func TestHttpGet(t *testing.T) {
// 對 foo 發送GET請求
resp, err := http.Get("http://localhost:8081/foo")
if err != nil {
t.Fatalf("Failed to send GET request: %v", err)
}
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
t.Log(string(body))
}
輸出結果是:{"name": "foo"}。
下面,我們使用 httpmock 來模擬我們的 http 請求,讓它返回我們想要的結果,請看下面示例:
package main
import (
"github.com/jarcoal/httpmock"
"io/ioutil"
"net/http"
"testing"
)
func TestHttpMockGetRequest(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
expectedResponse := "Hello, World!"
httpmock.RegisterResponder("GET", "http://localhost:8081/foo",
httpmock.NewStringResponder(200, expectedResponse))
httpmock.RegisterResponder("GET", "http://localhost:8081/bar",
httpmock.NewStringResponder(200, expectedResponse))
// 對 foo 發送GET請求
resp, err := http.Get("http://localhost:8081/foo")
if err != nil {
t.Fatalf("Failed to send GET request: %v", err)
}
// 驗證響應
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if string(body) != expectedResponse {
t.Errorf("Unexpected response. Expected: %s, Got: %s", expectedResponse, string(body))
}
// 對 bar 發送GET請求
resp, err = http.Get("http://localhost:8081/bar")
if err != nil {
t.Fatalf("Failed to send GET request: %v", err)
}
// 驗證響應
body, _ = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if string(body) != expectedResponse {
t.Errorf("Unexpected response. Expected: %s, Got: %s", expectedResponse, string(body))
}
count := httpmock.GetTotalCallCount()
t.Log(count)
}
現在,一起看解析一下上面代碼的含義。
-
httpmock.Activate: 啓動模擬環境,需要在測試運行之前調用。它將會把 http.DefaultClient 的 http.Client.Transport 字段替換爲 DefaultTransport。
-
httpmock.DeactivateAndReset: 與 Activate 成對出現,重置所做的改變。
-
httpmock.RegisterResponder: 註冊一個新的響應程序,該響應程序與給定的 HTTP 方法和 URL 路徑關聯。當收到匹配的請求參數時,將調用註冊的響應程序,並將結果放回給客戶端。
-
httpmock.GetTotalCallCount: 查看多少個用到了 httpmock 結果。
運行上面的測試用例,原先 foo 接口和 bar 接口分別返回的是 **{"name": "foo"} 和 **{"name": "bar"}****。經過我們修改後,它們都將返回 Hello, World! 與此同時,count 的值爲 2。
03 httpmock 高級示例
在上面的示例中,httpmock 直接匹配的是:請求方法和 URL 路徑。下面我們來看看 httpmock 提供的正則表達式匹配。
func TestHttpMockRegexpRequest(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", `=~^http://localhost:8081/(\w+)\z`,
func(req *http.Request) (*http.Response, error) {
name := httpmock.MustGetSubmatch(req, 1) // 1=first regexp submatch
fmt.Println(name)
return httpmock.NewJsonResponse(200, map[string]interface{}{
"name": "My name is " + name,
})
})
resp, err := http.Get("http://localhost:8081/bar")
if err != nil {
t.Fatalf("Failed to send GET request: %v", err)
}
// 驗證響應
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
t.Log(string(body))
}
與先前的示例不同,現在我們的 URL 是使用了正則表達式:
=~^http://localhost:8081/(\w+)\z
是一個正則表達式模式。讓我來解釋一下它的含義:
-
=~:這個符號表示進行正則表達式匹配操作。
-
^:在正則表達式中,^ 表示匹配文本的開頭位置。
-
http://localhost:8081/:這部分匹配了固定的文本 http://localhost:8081/。
-
\w+:這部分匹配一個或多個單詞字符。
-
\z:在正則表達式中,\z 表示匹配文本的結尾位置。
因此,我們輸入 http://localhost:8081/bar 作爲測試例子,就能夠被完全匹配了,並且輸出:{"name":"My name is bar"}。
04 resty 與 httpmock 結合
在日常開發中,我比較喜歡用 go-resty 庫,go-resty 庫是 Go 語言中一個簡單的 HTTP 和 Resty 客戶端庫,其涵蓋了比較多的功能,開箱即用,很方便。下面一起來看看如何把 resty 也使用 httpmock 請求。
首先,我們編寫一段客戶端代碼,用於請求上面定義的接口,代碼如下:
package main
import (
"context"
"fmt"
"github.com/go-resty/resty/v2"
)
func Request(client *resty.Client) (*Result, error) {
url := "http://localhost:8081"
result := &Result{}
resp, err := client.SetTimeout(time.Second * 5).
R().SetContext(context.Background()).SetResult(result).Get(url)
// 出現網絡等請求錯誤
if err != nil {
return nil, err
}
// 響應碼不是 200
if resp.StatusCode() != http.StatusOK {
return nil, fmt.Errorf("status code: %d", resp.StatusCode())
}
return result, nil
}
下面我們直接看看 Request 方法的測試用例如何寫:
func TestRequest(t *testing.T) {
client := resty.New()
method := "GET"
url := "http://localhost:8081"
convey.Convey("Test Request", t, func() {
// 1. 模擬出現網絡錯誤
convey.Convey("模擬出現網絡錯誤", func() {
httpmock.ActivateNonDefault(client.GetClient())
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(method, url, func(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("network error")
})
_, err := Request(client)
convey.So(err, convey.ShouldNotBeNil)
})
// 2. 模擬出現500錯誤碼
convey.Convey("模擬出現500錯誤碼", func() {
httpmock.ActivateNonDefault(client.GetClient())
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(method, url, httpmock.NewStringResponder(500, ""))
_, err := Request(client)
convey.So(err, convey.ShouldNotBeNil)
})
// 3. 模擬正常請求
convey.Convey("模擬正常請求", func() {
httpmock.ActivateNonDefault(client.GetClient())
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(method, url, func(req *http.Request) (*http.Response, error) {
return httpmock.NewJsonResponse(200, &Result{Name: "success"})
})
resp, err := Request(client)
convey.So(err, convey.ShouldBeNil)
convey.So(resp.Name, convey.ShouldEqual, "success")
})
})
}
一個完整的測試用例就呈現了,代碼的核心是:
httpmock.ActivateNonDefault(client.GetClient())
我們需要將 client 作爲參數傳入到 Request 方法中,纔可以讓 httpmock 去使用。
05 總結
httpmock 是一個用法比較簡單的庫,如果你平時有寫測試用例,應該是會用到的啦,這篇文章分享給你,希望對你有用!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/5TiJxK7i5ggizYWP_MbCxg