【Golang 接口自動化08】使用標准庫httptest完成HTTP請求的Mock測試


前言

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM