前言
Mock是一個做自動化測試永遠繞不過去的話題。本文主要介紹使用標准庫net/http/httptest
完成HTTP請求的Mock的測試方法。
可能有的小伙伴不太了解mock在實際自動化測試過程中的意義,在我的另外一篇博客中有比較詳細的描述,在本文中我們可以簡單理解為它可以解決測試依賴。下面我們一起來學習它。
http包的HandleFunc函數
我們在前面的文章中介紹過怎么發送各種http請求,但是沒有介紹過怎么使用golang啟動一個http的服務。我們首先來看看怎么使用golang建立一個服務。
使用golang啟動一個http服務非常簡單,把下面的代碼保存在httpServerDemo.go
中,執行命令go run httpServerDemo.go
就完成建立了一個監聽在http://127.0.0.1:9090/
上的服務。
package main
import (
"fmt"
"log"
"net/http"
)
func httpServerDemo(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"name":"Bingo","age":"18"}`)
}
func main() {
http.HandleFunc("/", httpServerDemo)
err := http.ListenAndServe(":9090", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
訪問http://127.0.0.1:9090/
可以看到下面的內容。
介紹如何建立一個服務,是因為我們要學習建立服務需要使用到的兩個結構體http.Request
/http.ResponseWriter
。下面我們一起來看看他們的具體內容。
http.Request/http.ResponseWriter
type Request struct {
Method string
URL *url.URL
Proto string
ProtoMajor int
ProtoMinor int
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
...
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
從上面的定義可以看到兩個結構體具體的參數和方法定義。下面我們一起來學習net/http/httptest
。
httptest
假設現在有這么一個場景,我們現在有一個功能需要調用免費天氣API來獲取天氣信息,但是這幾天該API升級改造暫時不提供聯調服務,而Boss希望該服務恢復后我們的新功能能直接上線,我們要怎么在服務不可用的時候完成相關的測試呢?答案就是使用Mock。
net/http/httptest
就是原生庫里面提供Mock服務的包,使用它不用真正的啟動一個http server
(亦或者請求任意的server),而且創建方法非常簡單。下面我們一起來看看怎么使用它吧。
定義被測接口
將下面的內容保存到weather.go
中:
package weather
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
const (
ADDRESS = "shenzhen"
)
type Weather struct {
City string `json:"city"`
Date string `json:"date"`
TemP string `json:"temP"`
Weather string `json:"weather"`
}
func GetWeatherInfo(api string) ([]Weather, error) {
url := fmt.Sprintf("%s/weather?city=%s", api, ADDRESS)
resp, err := http.Get(url)
if err != nil {
return []Weather{}, err
}
if resp.StatusCode != http.StatusOK {
return []Weather{}, fmt.Errorf("Resp is didn't 200 OK:%s", resp.Status)
}
bodybytes, _ := ioutil.ReadAll(resp.Body)
personList := make([]Weather, 0)
err = json.Unmarshal(bodybytes, &personList)
if err != nil {
fmt.Errorf("Decode data fail")
return []Weather{}, fmt.Errorf("Decode data fail")
}
return personList, nil
}
根據我們前面的場景設定,GetWeatherInfo
依賴接口是不可用的,所以resp, err := http.Get(url)
這一行的err肯定不為nil。為了不影響天氣服務恢復后我們的功能能直接上線,我們在不動源碼,從單元測試用例入手來完成測試。
測試代碼
將下面的內容保存到weather_test.go
中::
package weather
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
var weatherResp = []Weather{
{
City: "shenzhen",
Date: "10-22",
TemP: "15℃~21℃",
Weather: "rain",
},
{
City: "guangzhou",
Date: "10-22",
TemP: "15℃~21℃",
Weather: "sunny",
},
{
City: "beijing",
Date: "10-22",
TemP: "1℃~11℃",
Weather: "snow",
},
}
var weatherRespBytes, _ = json.Marshal(weatherResp)
func TestGetInfoUnauthorized(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
w.Write(weatherRespBytes)
if r.Method != "GET" {
t.Errorf("Except 'Get' got '%s'", r.Method)
}
if r.URL.EscapedPath() != "/weather" {
t.Errorf("Except to path '/person',got '%s'", r.URL.EscapedPath())
}
r.ParseForm()
topic := r.Form.Get("city")
if topic != "shenzhen" {
t.Errorf("Except rquest to have 'city=shenzhen',got '%s'", topic)
}
}))
defer ts.Close()
api := ts.URL
fmt.Printf("Url:%s\n", api)
resp, err := GetWeatherInfo(api)
if err != nil {
t.Errorf("ERR:", err)
} else {
fmt.Println("resp:", resp)
}
}
func TestGetInfoOK(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(weatherRespBytes)
if r.Method != "GET" {
t.Errorf("Except 'Get' got '%s'", r.Method)
}
if r.URL.EscapedPath() != "/weather" {
t.Errorf("Except to path '/person',got '%s'", r.URL.EscapedPath())
}
r.ParseForm()
topic := r.Form.Get("city")
if topic != "shenzhen" {
t.Errorf("Except rquest to have 'city=shenzhen',got '%s'", topic)
}
}))
defer ts.Close()
api := ts.URL
fmt.Printf("Url:%s\n", api)
resp, err := GetWeatherInfo(api)
if err != nil {
fmt.Println("ERR:", err)
} else {
fmt.Println("resp:", resp)
}
}
簡單解釋一下上面的部分代碼:
- 我們通過httptest.NewServer創建了一個測試的http server
- 通過變量r *http.Request讀請求設置,通過w http.ResponseWriter設置返回值
- 通過ts.URL來獲取請求的URL(一般都是http://ip:port)也就是實際的請求url
- 通過r.Method來獲取請求的方法,來測試判斷我們的請求方法是否正確
- 獲取請求路徑:r.URL.EscapedPath(),本例中的請求路徑就是"/weather"
- 獲取請求參數:r.ParseForm,r.Form.Get("city")
- 設置返回的狀態碼:w.WriteHeader(http.StatusOK)
- 設置返回的內容(也就是我們想要的結果):w.Write(personResponseBytes),注意w.Write()接收的參數是[]byte,所以通過json.Marshal(personResponse)轉換。
當然,我們也可以設置其他參數的值,也就是我們在最前面介紹的http.Request
/http.ResponseWriter
這兩個結構體的內容。
測試執行
在終端中進入我們保存上面兩個文件的目錄,執行go test -v
就可以看到下面的測試結果:
bingo@Mac httptest$ go test -v
=== RUN TestGetInfoUnauthorized
Url:http://127.0.0.1:55816
--- FAIL: TestGetInfoUnauthorized (0.00s)
person_test.go:55: ERR:%!(EXTRA *errors.errorString=Resp is didn't 200 OK:401 Unauthorized)
=== RUN TestGetInfoOK
Url:http://127.0.0.1:55818
resp: [{shenzhen 10-22 15℃~21℃ rain} {guangzhou 10-22 15℃~21℃ sunny} {beijing 10-22 1℃~11℃ snow}]
--- PASS: TestGetInfoOK (0.00s)
FAIL
exit status 1
FAIL bingo.com/blogs/httptest 0.016s
可以看到兩條測試用例成功了一條失敗了一條,失敗的原因就是我們設置的接口響應碼為401(w.WriteHeader(http.StatusUnauthorized)
),這個可能會在調用其他服務時遇到,所以有必要進行測試。更多的響應碼我們可以在我們的golang安裝目錄下找到,比如博主的路徑是:
/usr/local/go/src/net/http/status.go
這個文件中定義了幾乎所有的http響應碼:
StatusContinue = 100 // RFC 7231, 6.2.1
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusOK = 200 // RFC 7231, 6.3.1
StatusCreated = 201 // RFC 7231, 6.3.2
StatusAccepted = 202 // RFC 7231, 6.3.3
StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
StatusNoContent = 204 // RFC 7231, 6.3.5
StatusResetContent = 205 // RFC 7231, 6.3.6
...
綜上,我們可以通過不發送httptest來模擬出httpserver和返回值來進行自己代碼的測試,上面寫的兩條用例只是拋磚引玉,大家可以根據實際業務使用更多的場景來進行Mock。
總結
- httptest
- HandleFunc
- 結構體
http.Request
/http.ResponseWriter
- http 響應碼
參考資料:
【1】https://wizardforcel.gitbooks.io/golang-stdlib-ref/content/91.html
【2】https://blog.csdn.net/lavorange/article/details/73369153?utm_source=itdadao&utm_medium=referral