巴子 發表於 2021-7-22 23:39:00

Go进阶--httptest

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>基本使用</li><li>扩展使用</li><li>接口context使用</li><li>模拟调用</li><li>测试覆盖率</li><li>参考</li></ul></div><p></p>
<p>单元测试的原则,就是你所测试的函数方法,不要受到所依赖环境的影响,比如网络访问等,因为有时候我们运行单元测试的时候,并没有联网,那么总不能让单元测试因为这个失败吧?所以这时候模拟网络访问就有必要了。</p>
<p>对于go的web应用程序中往往需要与其他系统进行交互, 比如通过http访问其他系统, 此时就需要一种方法用于<code>打桩</code>来模拟Web服务端和客户端,httptest包即Go语言针对Web应用提供的解决方案。</p>
<p>httptest 可以方便的模拟各种Web服务器和客户端,以达到测试的目的。</p>
<h3 id="基本使用">基本使用</h3>
<p>假设在server中handler已经写好: main_test.go</p>
<pre><code class="language-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)
        }
}

</code></pre>
<p>测试代码如下:<br>
main.go</p>
<pre><code class="language-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)
    }
}
</code></pre>
<p>在不启动服务的情况下即可web接口的测试</p>
<pre><code class="language-shell"># 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-arguments0.031s
</code></pre>
<p>运行这个单元测试,就可以看到访问/health-check的结果里,并且我们没有启动任何HTTP服务就达到了目的。这个主要利用httptest.NewRecorder()创建一个http.ResponseWriter,模拟了真实服务端的响应,这种响应时通过调用http.DefaultServeMux.ServeHTTP方法触发的。</p>
<h3 id="扩展使用">扩展使用</h3>
<p>如果Web Server有操作数据库的行为,需要在init函数中进行数据库的连接。</p>
<p>参考官方文档中的样例编写的另外一个测试代码:</p>
<pre><code class="language-go">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)
    }
}
</code></pre>
<p>不同的地方:</p>
<ul>
<li>http.NewRequest替换为httptest.NewRequest。</li>
<li>httptest.NewRequest的第三个参数可以用来传递body数据,必须实现io.Reader接口。</li>
<li>httptest.NewRequest不会返回error,无需进行err!=nil检查。</li>
<li>解析响应时没直接使用ResponseRecorder,而是调用了Result函数。</li>
</ul>
<h3 id="接口context使用">接口context使用</h3>
<p>代码如下</p>
<pre><code class="language-go">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",
      &amp;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)
    }
}
</code></pre>
<h3 id="模拟调用">模拟调用</h3>
<p>还有一个模拟调用的方式,是真的在测试机上模拟一个服务器,然后进行调用测试。<br>
将上面的代码进行改造</p>
<p><em>main.go</em></p>
<pre><code class="language-go">package main

import (
        "encoding/json"
        "log"
        "net/http"
)

type Response struct {
        Code int64                  `json:"code"`
        Data mapinterface{} `json:"data"`
        Msgstring               `json:"msg"`
}

// json 返回
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
        // A very simple health check.
        response := Response{
                Code: 200,
                Msg:"ok",
                Data: mapinterface{}{"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)
        }
}
</code></pre>
<p><em>main_mock_test.go</em></p>
<pre><code class="language-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: mapinterface{}{"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\n", js)
}
</code></pre>
<p>模拟服务器的创建使用的是httptest.NewServer函数,它接收一个<code>http.Handler</code>处理API请求的接口。 代码示例中使用了Hander的适配器模式,<code>http.HandlerFunc</code>是一个函数类型,实现了http.Handler接口,这里是强制类型转换,不是函数的调用</p>
<p>执行测试:</p>
<pre><code class="language-shell"># 指令
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-arguments0.032s
</code></pre>
<h3 id="测试覆盖率">测试覆盖率</h3>
<p>尽可能的模拟更多的场景来测试我们代码的不同情况,但是有时候的确也有忘记测试的代码,这时候我们就需要测试覆盖率作为参考了。</p>
<p><strong>由单元测试的代码,触发运行到的被测试代码的代码行数占所有代码行数的比例,被称为测试覆盖率</strong>,代码覆盖率不一定完全精准,但是可以作为参考,可以帮我们测量和我们预计的覆盖率之间的差距。</p>
<p><em>main.go</em></p>
<pre><code class="language-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")

        }
}
</code></pre>
<p><em>main_test</em></p>
<pre><code class="language-go">func TestTag(t *testing.T) {
        Tag(1)
        Tag(2)

}
</code></pre>
<p>使用<code>go test</code>工具运行单元测试,和前几次不一样的是 ,要显示测试覆盖率,所以要多加一个参数-coverprofile,所以完整的命令为:<code>go test -v -coverprofile=c.out </code>,-coverprofile是指定生成的覆盖率文件,例子中是c.out,这个文件一会我们会用到。现在看终端输出,已经有了一个覆盖率。</p>
<pre><code class="language-shell"># 执行
$ 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-arguments0.005scoverage: 60.0% of statements
</code></pre>
<p>coverage: 60.0% of statements,60%的测试覆盖率,还没有到100%</p>
<p>那么看看还有那些代码没有被测试到。</p>
<p>这就需要我们刚刚生成的测试覆盖率文件c.out生成测试覆盖率报告了。生成报告有go为我们提供的工具,使用<code>go tool cover -html=c.out -o=tag.html</code> ,即可生成一个名字为tag.html的HTML格式的测试覆盖率报告,这里有详细的信息告诉我们哪一行代码测试到了,哪一行代码没有测试到。</p>
<p><img src="https://gitee.com/oneTotwo/images/raw/master/img/20210722233619.png" alt="" loading="lazy"></p>
<p>上图中可以看到,标记为绿色的代码行已经被测试了;标记为红色的还没有测试到,有2行的,现在我们根据没有测试到的代码逻辑,完善我的单元测试代码即可。</p>
<pre><code class="language-go">func TestTag(t *testing.T) {
        Tag(1)
        Tag(2)
        Tag(3)
        Tag(6)

}
</code></pre>
<p>单元测试完善为如上代码,再运行单元测试,就可以看到测试覆盖率已经是100%了,大功告成。</p>
<h3 id="参考">参考</h3>
<p>httptest 参考</p>


</div>
<div id="MySignature" role="contentinfo">
    ♥永远年轻,永远热泪盈眶♥<br><br>
来源:https://www.cnblogs.com/failymao/p/15046970.html
頁: [1]
查看完整版本: Go进阶--httptest