【Golang 接⼝⾃动化08】使⽤标准库httptest 完成HTTP 请求的
Mock 测试
Mock 是⼀个做⾃动化测试永远绕不过去的话题。本⽂主要介绍使⽤标准库net/http/httptest 完成HTTP 请求的Mock 的测试⽅法。
可能有的⼩伙伴不太了解mock 在实际⾃动化测试过程中的意义,在我的中有⽐较详细的描述,在本⽂中我们可以简单理解为它可以解决测试依赖。下⾯我们⼀起来学习它。
我们在前⾯的⽂章中介绍过怎么发送各种http 请求,但是没有介绍过怎么使⽤golang 启动⼀个http 的服务。我们⾸先来看看怎么使⽤golang 建⽴⼀个服务。
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.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 。
假设现在有这么⼀个场景,我们现在有⼀个功能需要调⽤免费天⽓API 来获取天⽓信息,但是这⼏天该API 升级改造暂时不提供联调服务,⽽Boss 希望该服务恢复后我们的新功能能直接上线,我们要怎么在服务不可⽤的时候完成相关的测试呢?答案就是使⽤Mock 。net/http/httptest 就是原⽣库⾥⾯提供Mock 服务的包,使⽤它不⽤真正的启动⼀个http server (亦或者请求任意的server ),⽽且创建⽅法⾮常简单。下⾯我们⼀起来看看怎么使⽤它吧。
定义被测接⼝
将下⾯的内容保存到 中:
前⾔
http 包的HandleFunc 函数
httptest
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。为了不影响天⽓服务恢复后我们的功能能直接上线,我们在不动源码,从单元测试⽤例⼊⼿来完成测试。
测试代码
将下⾯的内容保存到中::
package weather
import (
"encoding/json"
"fmt"
"net/http"
parse error怎么解决"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  TestGetInfoUnauthorized
Url:127.0.0.1:55816
--- FAIL: TestGetInfoUnauthorized (0.00s)
:55: ERR:%!(EXTRA *String=Resp is didn't 200 OK:401 Unauthorized)
=== RUN  TestGetInfoOK
Url: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/blogs/httptest        0.016s
可以看到两条测试⽤例成功了⼀条失败了⼀条,失败的原因就是我们设置的接⼝响应码为401(w.WriteHeader(http.StatusUnauthorized)),这个可能会在调⽤其他服务时遇到,所以有必要进⾏
测试。更多的响应码我们可以在我们的golang安装⽬录下到,⽐如博主的路径是:
/usr/local/go/src/net/
这个⽂件中定义了⼏乎所有的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 响应码

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。