简单的Go HTTP处理程序测试所有路径

huangapple go评论100阅读模式
英文:

Go simple http handler testing all paths

问题

我正在尝试在这个简单的HTTP处理程序文件中实现100%的代码覆盖率。

如果成功,该文件将写入默认的响应头,然后返回状态码200和"Pong",我已经在下面的代码中进行了测试。然而,写入默认头部也有可能会生成错误,此时预期会返回状态码500和"Internal Error"的响应体。

我正在努力弄清楚如何触发测试中的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 状态码不是200;得到 %v", w.Code)
	}

	if w.Body.String() != "Pong" {
		t.Errorf("Ping Handler 响应体不是Pong;得到 %v", w.Body.String())
	}
}

// 这个测试用例失败了,因为它和通过的成功用例设置相同
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 状态码不是500;得到 %v", w.Code)
	}

	if w.Body.String() != "Internal Server Error" {
		t.Errorf("Ping Handler 响应体不是Internal Server Error;得到 %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("尝试渲染未知的内容类型")
	}
}

编辑
另一个例子:

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

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

英文:

I'm trying to get a 100% code coverage on this simple http handler file.

The file writes the default response headers if successful and then returns 200 with "Pong" which I've tested below. However, there is also a possibility that writing the default headers will generate an error in which case a 500 response with Internal Error body is expected.

I'm struggling to figure out how to trigger the 500 response case in a test. The case would fail if for some reason the writeDefaultHeaders function call's 2nd parameter was changed to "html" for example as html is not a supported response content type in my service.

What is the idiomatic way to mock this call / hit this error branch in the code?

Thanks.

ping_handler_test.go

package main

import (
	&quot;net/http&quot;
	&quot;net/http/httptest&quot;
	&quot;testing&quot;
)

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

	PingHandler(w, req)

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

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

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

	PingHandler(w, req)

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

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

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

		PingHandler(w, req)
	}
}

ping_handler.go

package main

import (
	&quot;fmt&quot;
	&quot;net/http&quot;
)

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

	fmt.Fprintf(w, &quot;Pong&quot;)
}

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

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

Edit
Another Example:

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

In this case, how do I test json.Marshal returning an error?

答案1

得分: 3

这段代码中,作者提到了一种在测试中模拟函数调用或触发错误分支的惯用方法。通常情况下,为了进行测试,你可以使用公共接口并为你的代码提供一个实现(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)

希望对你有帮助!

英文:

> What is the idiomatic way to mock this call / hit this error branch in the code?

Usually for testing you want to use a public interface and either supply an implementation to your code (NewMyThing(hw HeaderWriter)) or use some other mechanism (like a DefaultHeaderWriter which you can swap out in your test).

Since this code is private you can just use a variable though:

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

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

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

	fmt.Fprintf(w, &quot;Pong&quot;)
}

And then swap it out in your test:

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

You probably want to set it back when you're done.

In my opinion swapping out a single function like this is not good testing practice. Tests should be against the public API so you can tinker with your code without having to rewrite your tests every time you make a single change.

Interface example:

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)

And then:

json_response, err := marshaler.Marshal(response)

答案2

得分: 1

除非我漏掉了什么,否则获取错误的方法是删除硬编码的&quot;text&quot;,并将您传递给contentType的任何内容作为请求。从请求中解析出它,然后传递给writeDefaultHeaders。传递的情况要么是&quot;text&quot;,要么是&quot;json&quot;,其他情况应该会给您一个错误,假设handleException按预期工作(您没有显示它)。

示例(当然,您不希望&quot;Content-Type&quot;头看起来像这样)

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())
	}
}

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")
	}
}
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/ping", PingHandler)
	http.ListenAndServe(":8080", nil)
}

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")
	}
}
英文:

Unless I am missing something the way to get an error is to remove the hardcoded &quot;text&quot; and have whatever you're passing as contentType be something in the request. Parse it out of the request and then pass it on to writeDefaultHeaders. Passing case is either &quot;text&quot; or &quot;json&quot;, everything else should give you your error, assuming handleException works as expected (you haven't shown it)

Example (of course you don't want the "Content-Type" header to look like this)

package main
import (
&quot;net/http&quot;
&quot;net/http/httptest&quot;
&quot;testing&quot;
)
func Test200PingHandler(t *testing.T) {
req, _ := http.NewRequest(&quot;GET&quot;, &quot;/ping&quot;, nil)
req.Header().Set(&quot;Content-Type&quot;, &quot;text&quot;)
//req.Header().Set(&quot;Content-Type&quot;, &quot;json&quot;)
w := httptest.NewRecorder()
PingHandler(w, req)
if w.Code != http.StatusOK {
t.Errorf(&quot;Ping Handler Status Code is NOT 200; got %v&quot;, w.Code)
}
if w.Body.String() != &quot;Pong&quot; {
t.Errorf(&quot;Ping Handler Response Body is NOT Pong; got %v&quot;, w.Body.String())
}
}
// This fails as it is the same setup as the passing success case
func Test500PingHandler(t *testing.T) {
req, _ := http.NewRequest(&quot;GET&quot;, &quot;/ping&quot;, nil)
req.Header().Set(&quot;Content-Type&quot;, &quot;fail&quot;)
w := httptest.NewRecorder()
PingHandler(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf(&quot;Ping Handler Status Code is NOT 500; got %v&quot;, w.Code)
}
if w.Body.String() != &quot;Internal Server Error&quot; {
t.Errorf(&quot;Ping Handler Response Body is NOT Internal Server Error; got %v&quot;, w.Body.String())
}
}

main

package main
import (
&quot;fmt&quot;
&quot;net/http&quot;
)
func PingHandler(w http.ResponseWriter, r *http.Request) {
err := writeDefaultHeaders(w, req.Header().Get(&quot;Content-Type&quot;))
if err != nil {
handleException(w, err)
return
}
fmt.Fprintf(w, &quot;Pong&quot;)
}
func writeDefaultHeaders(w http.ResponseWriter, contentType string) error {
w.Header().Set(&quot;X-Frame-Options&quot;, &quot;DENY&quot;)
w.Header().Set(&quot;X-Content-Type-Options&quot;, &quot;nosniff&quot;)
w.Header().Set(&quot;X-XSS-Protection&quot;, &quot;1;mode=block&quot;)
switch contentType {
case &quot;text&quot;:
w.Header().Set(&quot;Content-Type&quot;, &quot;text/plain; charset=utf-8&quot;)
return nil
case &quot;json&quot;:
w.Header().Set(&quot;Content-Type&quot;, &quot;application/json; charset=UTF-8&quot;)
return nil
default:
return errors.New(&quot;Attempting to render an unknown content type&quot;)
}
}

答案3

得分: 0

你所写的代码中,PingHandler中的这部分代码将永远不会被执行到:

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

因为你只有在writeDefaultHeaders传入的参数不是"text"或"json"时才会返回错误,而在PingHandler中你硬编码了"text",所以ping handler永远不会调用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")
}

另外,你可以根据一些请求条件(比如请求是否以.json结尾)来更改PingHandler中的contentType设置(你可能需要这样做来提供json或文本),这样你就可以以某种方式触发错误。目前,由于PingHandler只提供文本,错误代码是多余的,结果也无法测试。

英文:

As you have written it, this code will never be reached in PingHandler:

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

because the only place you return error is when writeDefaultHeaders is passed something other than text or json, and in PingHandler you hard-code "text", so ping handler will never call handleException, and the error handling is redundant. There is no other place you might return error in writeDefaultHeaders.

If you wish to test handleException, to see it returns a 500 error correctly (which is what you are asserting/testing in Test500PingHandler), just construct a PingHandlerFail function in the test file which sets an incorrect responseType and use that - there is no other way to trigger your error code.

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

Alternatively, change PingHandler to set contentType based on some request criteria like whether the request ends in .json or not (which you will presumably need to do in order to serve either json or text) so that you can trigger an error somehow - at present since PingHandler never serves anything but text, the error code is redundant and the result untestable.

huangapple
  • 本文由 发表于 2015年9月8日 10:56:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/32448559.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定