前言

net/http/httptest

可能有的小伙伴不太了解mock在实际自动化测试过程中的意义,在我的另外一篇博客中有比较详细的描述,在本文中我们可以简单理解为它可以解决测试依赖。下面我们一起来学习它。

http包的HandleFunc函数

我们在前面的文章中介绍过怎么发送各种http请求,但是没有介绍过怎么使用golang启动一个http的服务。我们首先来看看怎么使用golang建立一个服务。

httpServerDemo.gogo run httpServerDemo.gohttp://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.Requesthttp.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/httptesthttp 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
}
GetWeatherInforesp, err := http.Get(url)

测试代码

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.Requesthttp.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
w.WriteHeader(http.StatusUnauthorized)
/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。

总结

http.Requesthttp.ResponseWriter

参考资料:
【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