• 【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
    ", 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
    ", 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

  • 相关阅读:
    ios 工具大全,最全框架
    获取APP的启动图 -Launch Image
    OCP-052考试题库汇总(3)-CUUG内部解答版
    OCP-052考题汇总(2)-CUUG内部解答版
    OCP-052考题汇总(1)-CUUG内部解答版
    【第60题】【062题库】2019年OCP认证062考试新题
    【第59题】【062题库】2019年OCP认证062考试新题
    【第58题】【062题库】2019年OCP认证062考试新题
    【第57题】【062题库】2019年OCP认证062考试新题
    【第56题】【062题库】2019年OCP认证062考试新题
  • 原文地址:https://www.cnblogs.com/Detector/p/9769840.html
Copyright © 2020-2023  润新知