• Go进阶--httptest


    单元测试的原则,就是你所测试的函数方法,不要受到所依赖环境的影响,比如网络访问等,因为有时候我们运行单元测试的时候,并没有联网,那么总不能让单元测试因为这个失败吧?所以这时候模拟网络访问就有必要了。

    对于go的web应用程序中往往需要与其他系统进行交互, 比如通过http访问其他系统, 此时就需要一种方法用于打桩来模拟Web服务端和客户端,httptest包即Go语言针对Web应用提供的解决方案。

    httptest 可以方便的模拟各种Web服务器和客户端,以达到测试的目的。

    基本使用

    假设在server中handler已经写好: main_test.go

    package main
    
    import (
    	"io"
    	"log"
    	"net/http"
    )
    
    func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
    	// A very simple health check.
    	w.WriteHeader(http.StatusOK)
    	w.Header().Set("Content-Type", "application/json")
    
    	// In the future we could report back on the status of our DB, or our cache
    	// (e.g. Redis) by performing a simple PING, and include them in the response.
    	_, err := io.WriteString(w, `{"alive": true}`)
    	if err != nil {
    		log.Printf("reponse err ")
    	}
    }
    
    func main() {
    	// 路由与视图函数绑定
    	http.HandleFunc("/health-check", HealthCheckHandler)
    
    	// 启动服务,监听地址
    	err := http.ListenAndServe(":9999", nil)
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    
    

    测试代码如下:
    main.go

    import (
        "net/http"
        "net/http/httptest"
        "testing"
    )
    
    func TestHealthCheckHandler(t *testing.T) {
        //创建一个请求
        req, err := http.NewRequest("GET", "/health-check", nil)
        if err != nil {
            t.Fatal(err)
        }
    
        // 我们创建一个 ResponseRecorder (which satisfies http.ResponseWriter)来记录响应
        rr := httptest.NewRecorder()
    
        //直接使用HealthCheckHandler,传入参数rr,req
        HealthCheckHandler(rr, req)
    
        // 检测返回的状态码
        if status := rr.Code; status != http.StatusOK {
            t.Errorf("handler returned wrong status code: got %v want %v",
                status, http.StatusOK)
        }
    
        // 检测返回的数据
        expected := `{"alive": true}`
        if rr.Body.String() != expected {
            t.Errorf("handler returned unexpected body: got %v want %v",
                rr.Body.String(), expected)
        }
    }
    

    在不启动服务的情况下即可web接口的测试

    # go test -v main_test.go main.go
    
    # 输出
    root@failymao:/mnt/d/gopath/httptest# go test -v main_test.go main.go
    === RUN   TestHealthCheckHandler
    --- PASS: TestHealthCheckHandler (0.00s)
    PASS
    ok      command-line-arguments  0.031s
    

    运行这个单元测试,就可以看到访问/health-check的结果里,并且我们没有启动任何HTTP服务就达到了目的。这个主要利用httptest.NewRecorder()创建一个http.ResponseWriter,模拟了真实服务端的响应,这种响应时通过调用http.DefaultServeMux.ServeHTTP方法触发的。

    扩展使用

    如果Web Server有操作数据库的行为,需要在init函数中进行数据库的连接。

    参考官方文档中的样例编写的另外一个测试代码:

    func TestHealthCheckHandler2(t *testing.T) {
        reqData := struct {
            Info string `json:"info"`
        }{Info: "P123451"}
    
        reqBody, _ := json.Marshal(reqData)
        fmt.Println("input:", string(reqBody))
        // 使用httptes.NewRequest请求接口
        req := httptest.NewRequest(
            http.MethodPost,
            "/health-check",
            bytes.NewReader(reqBody),
        )
    
        req.Header.Set("userid", "wdt")
        req.Header.Set("commpay", "brk")
        
        // 使用server,返回的是reponser
        rr := httptest.NewRecorder()
        HealthCheckHandler(rr, req)
    
        result := rr.Result()
    
        body, _ := ioutil.ReadAll(result.Body)
        fmt.Println(string(body))
    
        if result.StatusCode != http.StatusOK {
            t.Errorf("expected status 200,",result.StatusCode)
        }
    }
    

    不同的地方:

    • http.NewRequest替换为httptest.NewRequest。
    • httptest.NewRequest的第三个参数可以用来传递body数据,必须实现io.Reader接口。
    • httptest.NewRequest不会返回error,无需进行err!=nil检查。
    • 解析响应时没直接使用ResponseRecorder,而是调用了Result函数。

    接口context使用

    代码如下

    func TestGetProjectsHandler(t *testing.T) {
        req, err := http.NewRequest("GET", "/api/users", nil)
        if err != nil {
            t.Fatal(err)
        }
    
        rr := httptest.NewRecorder()
        // e.g. func GetUsersHandler(ctx context.Context, w http.ResponseWriter, r *http.Request)
        handler := http.HandlerFunc(GetUsersHandler)
    
        // Populate the request's context with our test data.
        ctx := req.Context()
        ctx = context.WithValue(ctx, "app.auth.token", "abc123")
        ctx = context.WithValue(ctx, "app.user",
            &YourUser{ID: "qejqjq", Email: "user@example.com"})
        
        // Add our context to the request: note that WithContext returns a copy of
        // the request, which we must assign.
        req = req.WithContext(ctx)
        handler.ServeHTTP(rr, req)
    
        // Check the status code is what we expect.
        if status := rr.Code; status != http.StatusOK {
            t.Errorf("handler returned wrong status code: got %v want %v",
                status, http.StatusOK)
        }
    }
    

    模拟调用

    还有一个模拟调用的方式,是真的在测试机上模拟一个服务器,然后进行调用测试。
    将上面的代码进行改造

    main.go

    package main
    
    import (
    	"encoding/json"
    	"log"
    	"net/http"
    )
    
    type Response struct {
    	Code int64                  `json:"code"`
    	Data map[string]interface{} `json:"data"`
    	Msg  string                 `json:"msg"`
    }
    
    // json 返回
    func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
    	// A very simple health check.
    	response := Response{
    		Code: 200,
    		Msg:  "ok",
    		Data: map[string]interface{}{"alive": true},
    	}
    	// In the future we could report back on the status of our DB, or our cache
    	// (e.g. Redis) by performing a simple PING, and include them in the response.
    	res, err := json.Marshal(response)
    	// _, err := io.WriteString(w, `{"alive": true}`)
    	if err != nil {
    		http.Error(w, err.Error(), http.StatusInternalServerError)
    		log.Printf("reponse err ")
    		return
    	}
    	w.WriteHeader(http.StatusOK)
    	w.Header().Set("Content-Type", "application/json")
    
    	_, err = w.Write(res)
    	if err != nil {
    		log.Printf("reponse err ")
    	}
    }
    
    func main() {
    	// 路由与视图函数绑定
    	http.HandleFunc("/health-check", HealthCheckHandler)
    
    	// 启动服务,监听地址
    	err := http.ListenAndServe(":9999", nil)
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    

    main_mock_test.go

    package main
    
    import (
    	"encoding/json"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"net/http/httptest"
    	"testing"
    )
    
    func mockServer() *httptest.Server {
    	// API调用处理函数
    	healthHandler := func(rw http.ResponseWriter, r *http.Request) {
    		response := Response{
    			Code: 200,
    			Msg:  "ok",
    			Data: map[string]interface{}{"alive": true},
    		}
    
    		rw.Header().Set("Content-Type", "application/json")
    		rw.WriteHeader(http.StatusOK)
    
    		_ = json.NewEncoder(rw).Encode(response)
    	}
    
    	// 适配器转换
    	return httptest.NewServer(http.HandlerFunc(healthHandler))
    }
    
    func TestHealthCheck3(t *testing.T) {
    	// 创建一个模拟的服务器
    	server := mockServer()
    	defer server.Close()
    
    	// Get请求发往模拟服务器的地址
    	request, err := http.Get(server.URL)
    	if err != nil {
    		t.Fatal("创建Get失败")
    	}
    	defer request.Body.Close()
    
    	log.Println("code:", request.StatusCode)
    	js, err := ioutil.ReadAll(request.Body)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	log.Printf("body:%s
    ", js)
    }
    

    模拟服务器的创建使用的是httptest.NewServer函数,它接收一个http.Handler处理API请求的接口。 代码示例中使用了Hander的适配器模式,http.HandlerFunc是一个函数类型,实现了http.Handler接口,这里是强制类型转换,不是函数的调用

    执行测试:

    # 指令
    go test -v main_mock_test.go main.go
    
    # 输出
    === RUN   TestHealthCheck3
    2021/07/22 23:20:27 code: 200
    2021/07/22 23:20:27 body:{"code":200,"data":{"alive":true},"msg":"ok"}
    
    --- PASS: TestHealthCheck3 (0.01s)
    PASS
    ok      command-line-arguments  0.032s
    

    测试覆盖率

    尽可能的模拟更多的场景来测试我们代码的不同情况,但是有时候的确也有忘记测试的代码,这时候我们就需要测试覆盖率作为参考了。

    由单元测试的代码,触发运行到的被测试代码的代码行数占所有代码行数的比例,被称为测试覆盖率,代码覆盖率不一定完全精准,但是可以作为参考,可以帮我们测量和我们预计的覆盖率之间的差距。

    main.go

    func Tag(tag int){
    	switch tag {
    	case 1:
    		fmt.Println("Android")
    	case 2:
    		fmt.Println("Go")
    	case 3:
    		fmt.Println("Java")
    	default:
    		fmt.Println("C")
    
    	}
    }
    

    main_test

    func TestTag(t *testing.T) {
    	Tag(1)
    	Tag(2)
    
    }
    

    使用go test工具运行单元测试,和前几次不一样的是 ,要显示测试覆盖率,所以要多加一个参数-coverprofile,所以完整的命令为:go test -v -coverprofile=c.out ,-coverprofile是指定生成的覆盖率文件,例子中是c.out,这个文件一会我们会用到。现在看终端输出,已经有了一个覆盖率。

    # 执行
    $ go test -v -coverprofile=c.out main_test.go main.go
    
    # 输出
    === RUN   TestTag
    Android
    Go
    --- PASS: TestTag (0.00s)
    PASS
    coverage: 60.0% of statements
    ok      command-line-arguments  0.005s  coverage: 60.0% of statements
    

    coverage: 60.0% of statements,60%的测试覆盖率,还没有到100%

    那么看看还有那些代码没有被测试到。

    这就需要我们刚刚生成的测试覆盖率文件c.out生成测试覆盖率报告了。生成报告有go为我们提供的工具,使用go tool cover -html=c.out -o=tag.html ,即可生成一个名字为tag.html的HTML格式的测试覆盖率报告,这里有详细的信息告诉我们哪一行代码测试到了,哪一行代码没有测试到。

    上图中可以看到,标记为绿色的代码行已经被测试了;标记为红色的还没有测试到,有2行的,现在我们根据没有测试到的代码逻辑,完善我的单元测试代码即可。

    func TestTag(t *testing.T) {
    	Tag(1)
    	Tag(2)
    	Tag(3)
    	Tag(6)
    
    }
    

    单元测试完善为如上代码,再运行单元测试,就可以看到测试覆盖率已经是100%了,大功告成。

    参考

    httptest 参考

    ♥永远年轻,永远热泪盈眶♥
  • 相关阅读:
    18个Java开源CMS系统一览
    冒泡排序
    数据挖掘十大经典算法
    开源Java CMS建站程序推荐
    Oracle
    Oracle Procedure returning Ref Cursor in Entity Framework 4
    Field_II
    SharePoint Video Library
    ORACLE 导入dmp文件
    System.Diagnostics.Process.Start()。它的作用是调用外部的命令
  • 原文地址:https://www.cnblogs.com/failymao/p/15046970.html
Copyright © 2020-2023  润新知