Skip to content

Commit 5115a49

Browse files
authored
Merge pull request #118 from jarcoal/method-mistake
feat: NewNotFoundResponder() helps to detect a possible method mistake
2 parents 9d5cf74 + 7256da5 commit 5115a49

5 files changed

Lines changed: 81 additions & 14 deletions

File tree

internal/stack_tracer.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ type StackTracer struct {
1313
Err error
1414
}
1515

16-
func (n StackTracer) Error() string {
17-
if n.Err == nil {
16+
// Error implements error interface.
17+
func (s StackTracer) Error() string {
18+
if s.Err == nil {
1819
return ""
1920
}
20-
return n.Err.Error()
21+
return s.Err.Error()
22+
}
23+
24+
// Unwrap implements the interface needed by errors.Unwrap.
25+
func (s StackTracer) Unwrap() error {
26+
return s.Err
2127
}
2228

2329
// CheckStackTracer checks for specific error returned by

internal/stack_tracer_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ func TestStackTracer(t *testing.T) {
2121
if st.Error() != "foo" {
2222
t.Errorf("Error() returned <%s> instead of <foo>", st.Error())
2323
}
24+
25+
if werr := st.Unwrap(); werr != st.Err {
26+
t.Errorf("Unwrap() returned <%s> instead of <foo>", werr)
27+
}
2428
}
2529

2630
func TestCheckStackTracer(t *testing.T) {

response.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ import (
1818
"github.com/jarcoal/httpmock/internal"
1919
)
2020

21+
// fromThenKeyType is used by Then().
22+
type fromThenKeyType struct{}
23+
24+
var fromThenKey = fromThenKeyType{}
25+
26+
// suggestedMethodKeyType is used by NewNotFoundResponder().
27+
type suggestedMethodKeyType struct{}
28+
29+
var suggestedMethodKey = suggestedMethodKeyType{}
30+
2131
// Responder is a callback that receives an http request and returns
2232
// a mocked response.
2333
type Responder func(*http.Request) (*http.Response, error)
@@ -125,10 +135,6 @@ func (r Responder) Delay(d time.Duration) Responder {
125135
}
126136
}
127137

128-
type fromThenKeyType struct{}
129-
130-
var fromThenKey = fromThenKeyType{}
131-
132138
var errThenDone = errors.New("ThenDone")
133139

134140
// similar is simple but a bit tricky. Here we consider two Responder
@@ -330,9 +336,13 @@ func NewErrorResponder(err error) Responder {
330336
// at /go/src/runtime/asm_amd64.s:1337
331337
func NewNotFoundResponder(fn func(...interface{})) Responder {
332338
return func(req *http.Request) (*http.Response, error) {
339+
suggestedMethod, _ := req.Context().Value(suggestedMethodKey).(string)
340+
if suggestedMethod != "" {
341+
suggestedMethod = ", but one matches method " + suggestedMethod
342+
}
333343
return nil, internal.StackTracer{
334344
CustomFn: fn,
335-
Err: fmt.Errorf("Responder not found for %s %s", req.Method, req.URL),
345+
Err: fmt.Errorf("Responder not found for %s %s%s", req.Method, req.URL, suggestedMethod),
336346
}
337347
}
338348
}

transport.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package httpmock
22

33
import (
44
"bytes"
5+
"context"
56
"errors"
67
"fmt"
78
"net/http"
@@ -208,6 +209,10 @@ func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
208209
// we didn't find a responder, so fire the 'no responder' responder
209210
m.callCountInfo[internal.NoResponder]++
210211
m.totalCallCount++
212+
// give a hint to NewNotFoundResponder() if it is a possible method error
213+
if suggestedMethod != "" {
214+
req = req.WithContext(context.WithValue(req.Context(), suggestedMethodKey, suggestedMethod))
215+
}
211216
responder = m.noResponder
212217
}
213218
m.mu.Unlock()
@@ -552,7 +557,30 @@ func sortedQuery(m url.Values) string {
552557
}
553558

554559
// RegisterNoResponder is used to register a responder that is called
555-
// if no other responder is found. The default is httpmock.ConnectionFailure.
560+
// if no other responder is found. The default is httpmock.ConnectionFailure
561+
// that returns an error able to indicate a possible method mismatch.
562+
//
563+
// Use it in conjunction with NewNotFoundResponder to ensure that all
564+
// routes have been mocked:
565+
//
566+
// import (
567+
// "testing"
568+
// "github.com/jarcoal/httpmock"
569+
// )
570+
// ...
571+
// func TestMyApp(t *testing.T) {
572+
// ...
573+
// // Calls testing.Fatal with the name of Responder-less route and
574+
// // the stack trace of the call.
575+
// httpmock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal))
576+
//
577+
// Will abort the current test and print something like:
578+
// transport_test.go:735: Called from net/http.Get()
579+
// at /go/src/github.com/jarcoal/httpmock/transport_test.go:714
580+
// github.com/jarcoal/httpmock.TestCheckStackTracer()
581+
// at /go/src/testing/testing.go:865
582+
// testing.tRunner()
583+
// at /go/src/runtime/asm_amd64.s:1337
556584
func (m *MockTransport) RegisterNoResponder(responder Responder) {
557585
m.mu.Lock()
558586
m.noResponder = responder

transport_test.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestMockTransport(t *testing.T) {
4646
if err == nil {
4747
t.Fatal("An error should occur")
4848
}
49-
if !strings.Contains(err.Error(), NoResponderFound.Error()) {
49+
if !strings.HasSuffix(err.Error(), NoResponderFound.Error()) {
5050
t.Fatal(err)
5151
}
5252

@@ -60,7 +60,7 @@ func TestMockTransport(t *testing.T) {
6060
if err == nil {
6161
t.Fatal("An error should occur")
6262
}
63-
if !strings.Contains(err.Error(),
63+
if !strings.HasSuffix(err.Error(),
6464
NoResponderFound.Error()+" for method Get, but one matches method GET") {
6565
t.Fatal(err)
6666
}
@@ -74,7 +74,7 @@ func TestMockTransport(t *testing.T) {
7474
if err == nil {
7575
t.Fatal("An error should occur")
7676
}
77-
if !strings.Contains(err.Error(),
77+
if !strings.HasSuffix(err.Error(),
7878
NoResponderFound.Error()+" for method POST, but one matches method GET") {
7979
t.Fatal(err)
8080
}
@@ -88,7 +88,7 @@ func TestMockTransport(t *testing.T) {
8888
if err == nil {
8989
t.Fatal("An error should occur")
9090
}
91-
if !strings.Contains(err.Error(),
91+
if !strings.HasSuffix(err.Error(),
9292
NoResponderFound.Error()+" for method POST, but one matches method GET") {
9393
t.Fatal(err)
9494
}
@@ -182,8 +182,27 @@ func TestMockTransportNoResponder(t *testing.T) {
182182
if err != nil {
183183
t.Fatal("expected request to succeed")
184184
}
185-
186185
assertBody(t, resp, "hello world")
186+
187+
// Using NewNotFoundResponder()
188+
RegisterNoResponder(NewNotFoundResponder(nil))
189+
_, err = http.Get(testURL)
190+
if err == nil {
191+
t.Fatal("an error should occur")
192+
}
193+
if !strings.HasSuffix(err.Error(), "Responder not found for GET http://www.example.com/") {
194+
t.Fatalf("Unexpected error content: %s", err)
195+
}
196+
197+
// Help the user in case a Responder exists for another method
198+
RegisterResponder("POST", testURL, NewStringResponder(200, "hello world"))
199+
_, err = http.Get(testURL)
200+
if err == nil {
201+
t.Fatal("an error should occur")
202+
}
203+
if !strings.HasSuffix(err.Error(), "Responder not found for GET http://www.example.com/, but one matches method POST") {
204+
t.Fatalf("Unexpected error content: %s", err)
205+
}
187206
}
188207

189208
func TestMockTransportQuerystringFallback(t *testing.T) {

0 commit comments

Comments
 (0)