前言
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 mainimport ( "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 weatherimport ( "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 weatherimport ( "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(一般都是)也就是实际的请求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 TestGetInfoUnauthorizedUrl: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 TestGetInfoOKUrl:http://127.0.0.1:55818resp: [{shenzhen 10-22 15℃~21℃ rain} {guangzhou 10-22 15℃~21℃ sunny} {beijing 10-22 1℃~11℃ snow}]--- PASS: TestGetInfoOK (0.00s)FAILexit status 1FAIL 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