目录
前言
http包的HandleFunc函数
http.Request/http.ResponseWriter
httptest
定义被测接口
测试代码
测试执行
总结
资料获取方法
前言
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 stringURL *url.URLProto stringProtoMajor intProtoMinor intHeader HeaderBody io.ReadCloserGetBody func() (io.ReadCloser, error)ContentLength int64TransferEncoding []stringClose bool
...
type ResponseWriter interface {Header() HeaderWrite([]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.URLfmt.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.URLfmt.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.1StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2StatusProcessing = 102 // RFC 2518, 10.1StatusOK = 200 // RFC 7231, 6.3.1StatusCreated = 201 // RFC 7231, 6.3.2StatusAccepted = 202 // RFC 7231, 6.3.3StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4StatusNoContent = 204 // RFC 7231, 6.3.5StatusResetContent = 205 // RFC 7231, 6.3.6...
综上,我们可以通过不发送httptest来模拟出httpserver和返回值来进行自己代码的测试,上面写的两条用例只是抛砖引玉,大家可以根据实际业务使用更多的场景来进行Mock。
总结
- httptest
- HandleFunc
- 结构体
http.Request
/http.ResponseWriter
- http 响应码
资料获取方法
【留言777】
各位想获取源码等教程资料的朋友请点赞 + 评论 + 收藏,三连!
三连之后我会在评论区挨个私信发给你们~