diff --git a/response.go b/response.go index 1394ec7..5f3b268 100644 --- a/response.go +++ b/response.go @@ -4,8 +4,11 @@ import ( "bytes" "encoding/json" "encoding/xml" + "errors" + "fmt" "io" "net/http" + "runtime" "strconv" "strings" ) @@ -29,6 +32,68 @@ func NewErrorResponder(err error) Responder { } } +// NewNotFoundResponder creates a Responder typically used in +// conjunction with RegisterNoResponder() function and testing +// package, to be proactive when a Responder is not found. fn is +// called with a unique string parameter containing the name of the +// missing route and the stack trace to localize the origin of the +// call. If fn returns (= if it does not panic), the responder returns +// an error of the form: "Responder not found for GET http://foo.bar/path". +// Note that fn can be nil. +// +// It is useful when writing tests to ensure that all routes have been +// mocked. +// +// Example of use: +// import "testing" +// ... +// func TestMyApp(t *testing.T) { +// ... +// // Calls testing.Fatal with the name of Responder-less route and +// // the stack trace of the call. +// httpmock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal)) +// +// Will abort the current test and print something like: +// response:69: Responder not found for: GET http://foo.bar/path +// Called from goroutine 20 [running]: +// github.com/jarcoal/httpmock.NewNotFoundResponder.func1(0xc00011f000, 0x0, 0x42dfb1, 0x77ece8) +// /go/src/github.com/jarcoal/httpmock/response.go:67 +0x1c1 +// github.com/jarcoal/httpmock.runCancelable(0xc00004bfc0, 0xc00011f000, 0x7692f8, 0xc, 0xc0001208b0) +// /go/src/github.com/jarcoal/httpmock/transport.go:146 +0x7e +// github.com/jarcoal/httpmock.(*MockTransport).RoundTrip(0xc00005c980, 0xc00011f000, 0xc00005c980, 0x0, 0x0) +// /go/src/github.com/jarcoal/httpmock/transport.go:140 +0x19d +// net/http.send(0xc00011f000, 0x7d3440, 0xc00005c980, 0x0, 0x0, 0x0, 0xc000010400, 0xc000047bd8, 0x1, 0x0) +// /usr/local/go/src/net/http/client.go:250 +0x461 +// net/http.(*Client).send(0x9f6e20, 0xc00011f000, 0x0, 0x0, 0x0, 0xc000010400, 0x0, 0x1, 0x9f7ac0) +// /usr/local/go/src/net/http/client.go:174 +0xfb +// net/http.(*Client).do(0x9f6e20, 0xc00011f000, 0x0, 0x0, 0x0) +// /usr/local/go/src/net/http/client.go:641 +0x279 +// net/http.(*Client).Do(...) +// /usr/local/go/src/net/http/client.go:509 +// net/http.(*Client).Get(0x9f6e20, 0xc00001e420, 0x23, 0xc00012c000, 0xb, 0x600) +// /usr/local/go/src/net/http/client.go:398 +0x9e +// net/http.Get(...) +// /usr/local/go/src/net/http/client.go:370 +// foo.bar/foobar/foobar.TestMyApp(0xc00011e000) +// /go/src/foo.bar/foobar/foobar/my_app_test.go:272 +0xdbb +// testing.tRunner(0xc00011e000, 0x77e3a8) +// /usr/local/go/src/testing/testing.go:865 +0xc0 +// created by testing.(*T).Run +// /usr/local/go/src/testing/testing.go:916 +0x35a +func NewNotFoundResponder(fn func(...interface{})) Responder { + return func(req *http.Request) (*http.Response, error) { + mesg := fmt.Sprintf("Responder not found for %s %s", req.Method, req.URL) + if fn != nil { + buf := make([]byte, 4096) + n := runtime.Stack(buf, false) + buf = buf[:n] + fn(mesg + "\nCalled from " + + strings.Replace(strings.TrimSuffix(string(buf), "\n"), "\n", "\n ", -1)) + } + return nil, errors.New(mesg) + } +} + // NewStringResponse creates an *http.Response with a body based on the given string. Also accepts // an http status code. func NewStringResponse(status int, body string) *http.Response { diff --git a/response_test.go b/response_test.go index f0835ef..a99480a 100644 --- a/response_test.go +++ b/response_test.go @@ -4,8 +4,10 @@ import ( "encoding/json" "encoding/xml" "errors" + "fmt" "io/ioutil" "net/http" + "strings" "testing" ) @@ -48,6 +50,54 @@ func TestResponderFromResponse(t *testing.T) { } } +func TestNewNotFoundResponder(t *testing.T) { + var mesg string + responder := NewNotFoundResponder(func(args ...interface{}) { + mesg = fmt.Sprint(args[0]) + }) + + req, err := http.NewRequest("GET", "http://foo.bar/path", nil) + if err != nil { + t.Fatal("Error creating request") + } + + const title = "Responder not found for GET http://foo.bar/path" + + resp, err := responder(req) + if resp != nil { + t.Error("resp should be nil") + } + if err == nil { + t.Error("err should be not nil") + } else if err.Error() != title { + t.Errorf(`err mismatch, got: "%s", expected: "%s"`, + err.Error(), + "Responder not found for: GET http://foo.bar/path") + } + + if !strings.HasPrefix(mesg, title+"\nCalled from ") { + t.Error(`mesg should begin with "` + title + `\nCalled from ", but it is: "` + mesg + `"`) + } + if strings.HasSuffix(mesg, "\n") { + t.Error(`mesg should not end with \n, but it is: "` + mesg + `"`) + } + + // nil fn + responder = NewNotFoundResponder(nil) + + resp, err = responder(req) + if resp != nil { + t.Error("resp should be nil") + } + if err == nil { + t.Error("err should be not nil") + } else if err.Error() != title { + t.Errorf(`err mismatch, got: "%s", expected: "%s"`, + err.Error(), + "Responder not found for: GET http://foo.bar/path") + } +} + func TestNewStringResponse(t *testing.T) { body := "hello world" status := 200