【问题标题】:Go simple http handler testing all pathsGo 简单的 http 处理程序测试所有路径
【发布时间】:2015-12-03 14:14:19
【问题描述】:

我试图在这个简单的 http 处理程序文件上获得 100% 的代码覆盖率。

如果成功,该文件将写入默认响应标头,然后返回 200 并带有我在下面测试过的“Pong”。但是,写入默认标头也有可能会产生错误,在这种情况下,预期会出现带有内部错误正文的 500 响应。

我正在努力弄清楚如何在测试中触发 500 响应案例。如果由于某种原因将 writeDefaultHeaders 函数调用的第二个参数更改为“html”,则案例将失败,例如 html 在我的服务中不是受支持的响应内容类型。

在代码中模拟这个调用/点击这个错误分支的惯用方法是什么?

谢谢。

ping_handler_test.go

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func Test200PingHandler(t *testing.T) {
    req, _ := http.NewRequest("GET", "/ping", nil)
    w := httptest.NewRecorder()

    PingHandler(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("Ping Handler Status Code is NOT 200; got %v", w.Code)
    }

    if w.Body.String() != "Pong" {
        t.Errorf("Ping Handler Response Body is NOT Pong; got %v", w.Body.String())
    }
}

// This fails as it is the same setup as the passing success case
func Test500PingHandler(t *testing.T) {
    req, _ := http.NewRequest("GET", "/ping", nil)
    w := httptest.NewRecorder()

    PingHandler(w, req)

    if w.Code != http.StatusInternalServerError {
        t.Errorf("Ping Handler Status Code is NOT 500; got %v", w.Code)
    }

    if w.Body.String() != "Internal Server Error" {
        t.Errorf("Ping Handler Response Body is NOT Internal Server Error; got %v", w.Body.String())
    }
}

func BenchmarkPingHandler(b *testing.B) {
    for i := 0; i < b.N; i++ {
        req, _ := http.NewRequest("GET", "/ping", nil)
        w := httptest.NewRecorder()

        PingHandler(w, req)
    }
}

ping_handler.go

package main

import (
    "fmt"
    "net/http"
)

func PingHandler(w http.ResponseWriter, r *http.Request) {
    err := writeDefaultHeaders(w, "text")
    if err != nil {
        handleException(w, err)
        return
    }

    fmt.Fprintf(w, "Pong")
}

func writeDefaultHeaders(w http.ResponseWriter, contentType string) error {
    w.Header().Set("X-Frame-Options", "DENY")
    w.Header().Set("X-Content-Type-Options", "nosniff")
    w.Header().Set("X-XSS-Protection", "1;mode=block")

    switch contentType {
    case "text":
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        return nil
    case "json":
        w.Header().Set("Content-Type", "application/json; charset=UTF-8")
        return nil
    default:
        return errors.New("Attempting to render an unknown content type")
    }
}

编辑 另一个例子:

json_response, err := json.Marshal(response)
if err != nil {
    handleException(w, err)
    return
}

在这种情况下,如何测试 json.Marshal 返回错误?

【问题讨论】:

  • 你试过只使用PingHandler(nil, nil)吗?这应该会导致我假设的错误?我没有你所有的代码,所以我无法运行和测试
  • 可以添加writeDefaultHeaders的内容吗?直接(可测试)的方法是编写您自己的嵌入 http.ResponseWriter 的类型,然后覆盖您要测试的方法。
  • @elithrar 已将其添加到问题中。
  • @user3591723 nil,nil 不起作用,因为那时我没有我的 writer 记录器来比较状态代码和正文。我已经添加了我的损坏测试以进行澄清。
  • 是的,它会导致恐慌,而不是错误。不过,我一定遗漏了一些东西,如果您希望 writeDefaultHeaders 返回错误,您不能将 "text" 硬编码为 contentType。只需让 contentType 来自请求并发出错误的请求 - 没有您期望的内容类型。

标签: unit-testing testing go tdd


【解决方案1】:

在代码中模拟这个调用/点击这个错误分支的惯用方法是什么?

通常对于测试,您希望使用公共接口并为您的代码提供一个实现 (NewMyThing(hw HeaderWriter)) 或使用其他机制(如您可以在测试中换掉的 DefaultHeaderWriter)。

由于这段代码是私有的,你可以只使用一个变量:

var writeDefaultHeaders = func(w http.ResponseWriter, contentType string) error {
    w.Header().Set("X-Frame-Options", "DENY")
    w.Header().Set("X-Content-Type-Options", "nosniff")
    w.Header().Set("X-XSS-Protection", "1;mode=block")

    switch contentType {
    case "text":
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        return nil
    case "json":
        w.Header().Set("Content-Type", "application/json; charset=UTF-8")
        return nil
    default:
        return errors.New("Attempting to render an unknown content type")
    }
}

func PingHandler(w http.ResponseWriter, r *http.Request) {
    err := writeDefaultHeaders(w, "text")
    if err != nil {
        handleException(w, err)
        return
    }

    fmt.Fprintf(w, "Pong")
}

然后在你的测试中换掉它:

func Test500PingHandler(t *testing.T) {
    writeDefaultHeaders = headerWriterFunc(func(w http.ResponseWriter, contentType string) error {
        return fmt.Errorf("ERROR")
    })
    // ...
}

您可能想在完成后将其重新设置。

在我看来,像这样换掉一个函数并不是好的测试实践。测试应该针对公共 API,这样您就可以修改您的代码,而不必在每次进行单个更改时都重写您的测试。

接口示例:

type Marshaler interface {
    Marshal(v interface{}) ([]byte, error)
}

type jsonMarshaler struct{}

func (_ *jsonMarshaler) Marshal(v interface{}) ([]byte, error) {
    return json.Marshal(v)
}

var marshaler Marshaler = (*jsonMarshaler)(nil)

然后:

json_response, err := marshaler.Marshal(response)

【讨论】:

  • 这基本上就是我想要的。谢谢。你能举例说明接口方法是如何工作的吗?例如,我使用 stdlib 调用对我的帖子进行了编辑,该调用可能返回导致触发错误条件块的错误。
【解决方案2】:

除非我遗漏了什么,否则得到错误的方法是删除硬编码的"text",并将你作为contentType 传递的任何内容都包含在请求中。将其从请求中解析出来,然后将其传递给 writeDefaultHeaders。传递大小写是"text""json",假设handleException 按预期工作(您尚未显示),其他所有内容都应该给您错误。

示例(当然您不希望“Content-Type”标头看起来像这样)

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func Test200PingHandler(t *testing.T) {
    req, _ := http.NewRequest("GET", "/ping", nil)
    req.Header().Set("Content-Type", "text")
    //req.Header().Set("Content-Type", "json")
    w := httptest.NewRecorder()

    PingHandler(w, req)

    if w.Code != http.StatusOK {
        t.Errorf("Ping Handler Status Code is NOT 200; got %v", w.Code)
    }

    if w.Body.String() != "Pong" {
        t.Errorf("Ping Handler Response Body is NOT Pong; got %v", w.Body.String())
    }
}

// This fails as it is the same setup as the passing success case
func Test500PingHandler(t *testing.T) {
    req, _ := http.NewRequest("GET", "/ping", nil)
    req.Header().Set("Content-Type", "fail")
    w := httptest.NewRecorder()

    PingHandler(w, req)

    if w.Code != http.StatusInternalServerError {
        t.Errorf("Ping Handler Status Code is NOT 500; got %v", w.Code)
    }

    if w.Body.String() != "Internal Server Error" {
        t.Errorf("Ping Handler Response Body is NOT Internal Server Error; got %v", w.Body.String())
    }
}

主要

package main

import (
    "fmt"
    "net/http"
)

func PingHandler(w http.ResponseWriter, r *http.Request) {
    err := writeDefaultHeaders(w, req.Header().Get("Content-Type"))
    if err != nil {
        handleException(w, err)
        return
    }

    fmt.Fprintf(w, "Pong")
}

func writeDefaultHeaders(w http.ResponseWriter, contentType string) error {
    w.Header().Set("X-Frame-Options", "DENY")
    w.Header().Set("X-Content-Type-Options", "nosniff")
    w.Header().Set("X-XSS-Protection", "1;mode=block")

    switch contentType {
    case "text":
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        return nil
    case "json":
        w.Header().Set("Content-Type", "application/json; charset=UTF-8")
        return nil
    default:
        return errors.New("Attempting to render an unknown content type")
    }
}

【讨论】:

  • 感谢您的回答,但这并不是我所希望的。没错,我可以在请求中使用 Content Type 或 Accept 来遵循 http 规范,但我不希望响应类型由用户控制。我来 Ruby 时,我希望模拟对 writeDefaultHeaders 的函数调用,以获得错误的输入参数或类似的东西。
  • 那么你需要某种逻辑来找出contentType。这只是一个使用“Content-Type”标头的示例,因为我必须想出一些办法。如果你有正确的大小写硬编码,你永远不会失败,就这么简单......
【解决方案3】:

正如您所写,PingHandler 中永远无法访问此代码:

if err != nil {
    handleException(w, err)
    return
}

因为您返回错误的唯一地方是 writeDefaultHeaders 传递的不是文本或 json 的内容,并且在 PingHandler 中您硬编码“文本”,因此 ping 处理程序永远不会调用 handleException,并且错误处理是多余的。在 writeDefaultHeaders 中没有其他地方可能会返回错误。

如果您希望测试 handleException,要查看它正确返回 500 错误(这是您在 Test500PingHandler 中断言/测试的内容),只需在测试文件中构造一个 PingHandlerFail 函数,该函数设置错误的 responseType 并使用它 - 那里没有其他方法可以触发您的错误代码。

func PingHandlerFail(w http.ResponseWriter, r *http.Request) {
    err := writeDefaultHeaders(w, "foo")
    if err != nil {
        handleException(w, err)
        return
    }
    fmt.Fprintf(w, "Pong")
}

或者,更改 PingHandler 以根据某些请求标准设置 contentType,例如请求是否以 .json 结尾(您可能需要这样做以提供 json 或文本),以便您可以以某种方式触发错误- 目前,由于 PingHandler 只提供文本,因此错误代码是多余的,结果无法测试。

【讨论】:

    猜你喜欢
    • 2023-01-04
    • 1970-01-01
    • 1970-01-01
    • 2013-10-24
    • 1970-01-01
    • 2015-12-26
    • 1970-01-01
    • 2021-01-21
    • 1970-01-01
    相关资源
    最近更新 更多