diff --git a/apitest.go b/apitest.go index 07015f2..41a7aac 100644 --- a/apitest.go +++ b/apitest.go @@ -485,7 +485,7 @@ func (a *APITest) runTest() (*httptest.ResponseRecorder, *http.Request) { } } - a.request.handler.ServeHTTP(res, req) + a.serveHttp(res, req) if a.debugEnabled { responseDump, err := httputil.DumpResponse(res.Result(), true) @@ -497,6 +497,16 @@ func (a *APITest) runTest() (*httptest.ResponseRecorder, *http.Request) { return res, req } +func (a *APITest) serveHttp(res *httptest.ResponseRecorder, req *http.Request) { + defer func() { + if err := recover(); err != nil { + a.t.Fatal(err) + } + }() + + a.request.handler.ServeHTTP(res, req) +} + func (a *APITest) BuildRequest() *http.Request { req, _ := http.NewRequest(a.request.method, a.request.url, bytes.NewBufferString(a.request.body)) req.URL.RawQuery = formatQuery(a.request) diff --git a/assert/assert.go b/assert/assert.go index 20be082..9a2b970 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -49,7 +49,7 @@ func Equal(t *testing.T, expected, actual interface{}, message ...string) { if len(message) > 0 { t.Fatalf(strings.Join(message, ", ")) } else { - t.Fatalf("Expected %+v but recevied %+v", expected, actual) + t.Fatalf("Expected '%+v' but recevied '%+v'", expected, actual) } } } diff --git a/mocks.go b/mocks.go index 87e54da..53b37a9 100644 --- a/mocks.go +++ b/mocks.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "io/ioutil" "net/http" "net/http/httputil" @@ -11,13 +12,10 @@ import ( "net/url" "reflect" "regexp" + "sort" "strings" ) -var ( - ErrFailedToMatch = "failed to match any of the defined mocks" -) - type Transport struct { debugEnabled bool mocks []*Mock @@ -49,26 +47,63 @@ func newTransport( return t } -func (r *Transport) RoundTrip(req *http.Request) (*http.Response, error) { - var responseMock *http.Response +type unmatchedMockError struct { + errors map[int][]error +} + +func newUnmatchedMockError() *unmatchedMockError { + return &unmatchedMockError{ + errors: map[int][]error{}, + } +} + +func (u *unmatchedMockError) addErrors(mockNumber int, errors ...error) *unmatchedMockError { + u.errors[mockNumber] = append(u.errors[mockNumber], errors...) + return u +} + +func (u *unmatchedMockError) Error() string { + var strBuilder strings.Builder + strBuilder.WriteString("received request did not match any mocks\n\n") + for _, mockNumber := range u.orderedMockKeys() { + strBuilder.WriteString(fmt.Sprintf("Mock %d mismatches:\n", mockNumber)) + for _, err := range u.errors[mockNumber] { + strBuilder.WriteString("• ") + strBuilder.WriteString(err.Error()) + strBuilder.WriteString("\n") + } + strBuilder.WriteString("\n") + } + return strBuilder.String() +} + +func (u *unmatchedMockError) orderedMockKeys() []int { + var mockKeys []int + for mockKey := range u.errors { + mockKeys = append(mockKeys, mockKey) + } + sort.Ints(mockKeys) + return mockKeys +} +// RoundTrip implementation intended to match a given expected mock request or throw an error with a list of reasons why no match was found. +func (r *Transport) RoundTrip(req *http.Request) (mockResponse *http.Response, matchErrors error) { if r.debugEnabled { defer func() { - debugMock(responseMock, req) + debugMock(mockResponse, req) }() } if r.observe != nil { defer func() { - r.observe(responseMock, req, r.apiTest) + r.observe(mockResponse, req, r.apiTest) }() } - if matchedResponse := matches(req, r.mocks); matchedResponse != nil { - responseMock = buildResponseFromMock(matchedResponse) - return responseMock, nil + if matchedResponse, matchErrors := matches(req, r.mocks); matchErrors == nil { + return buildResponseFromMock(matchedResponse), nil } - return nil, errors.New(ErrFailedToMatch) + return nil, matchErrors } func debugMock(res *http.Response, req *http.Request) { @@ -231,26 +266,29 @@ func (m *Mock) Method(method string) *MockRequest { return m.request } -func matches(req *http.Request, mocks []*Mock) *MockResponse { - for _, mock := range mocks { +func matches(req *http.Request, mocks []*Mock) (*MockResponse, error) { + mockError := newUnmatchedMockError() + for mockNumber, mock := range mocks { if mock.isUsed { continue } - matches := true + var mockMatchErrors []error for _, matcher := range matchers { - if !matcher(req, mock.request) { - matches = false - break + if matcherError := matcher(req, mock.request); matcherError != nil { + mockMatchErrors = append(mockMatchErrors, matcherError) } } - if matches { + if len(mockMatchErrors) == 0 { mock.isUsed = true - return mock.response + return mock.response, nil } + + mockError = mockError.addErrors(mockNumber+1, mockMatchErrors...) } - return nil + + return nil, mockError } func (r *MockRequest) Body(b string) *MockRequest { @@ -336,56 +374,74 @@ func (r *MockResponse) End() *Mock { return r.mock } -type Matcher func(*http.Request, *MockRequest) bool +// Matcher type accepts the actual request and a mock request to match against. +// Will return an error that describes why there was a mismatch if the inputs do not match or nil if they do. +type Matcher func(*http.Request, *MockRequest) error -var pathMatcher Matcher = func(r *http.Request, spec *MockRequest) bool { - if r.URL.Path == spec.url.Path { - return true +var pathMatcher Matcher = func(r *http.Request, spec *MockRequest) error { + receivedPath := r.URL.Path + mockPath := spec.url.Path + if receivedPath == mockPath { + return nil } - matched, err := regexp.MatchString(spec.url.Path, r.URL.Path) - return matched && err == nil + matched, err := regexp.MatchString(mockPath, receivedPath) + return errorOrNil(matched && err == nil, func() string { + return fmt.Sprintf("received path %s did not match mock path %s", receivedPath, mockPath) + }) } -var hostMatcher Matcher = func(r *http.Request, spec *MockRequest) bool { - if spec.url.Host == "" { - return true +var hostMatcher Matcher = func(r *http.Request, spec *MockRequest) error { + receivedHost := r.Host + mockHost := spec.url.Host + if mockHost == "" { + return nil } - if r.Host == spec.url.Host { - return true + if receivedHost == mockHost { + return nil } - matched, err := regexp.MatchString(spec.url.Host, r.URL.Path) - return matched && err != nil + matched, err := regexp.MatchString(mockHost, r.URL.Path) + return errorOrNil(matched && err != nil, func() string { + return fmt.Sprintf("received host %s did not match mock host %s", receivedHost, mockHost) + }) } -var methodMatcher Matcher = func(r *http.Request, spec *MockRequest) bool { - if r.Method == spec.method { - return true +var methodMatcher Matcher = func(r *http.Request, spec *MockRequest) error { + receivedMethod := r.Method + mockMethod := spec.method + if receivedMethod == mockMethod { + return nil } - if spec.method == "" { - return true + if mockMethod == "" { + return nil } - return false + return fmt.Errorf("received method %s did not match mock method %s", receivedMethod, mockMethod) } -var schemeMatcher Matcher = func(r *http.Request, spec *MockRequest) bool { - if r.URL.Scheme == "" { - return true +var schemeMatcher Matcher = func(r *http.Request, spec *MockRequest) error { + receivedScheme := r.URL.Scheme + mockScheme := spec.url.Scheme + if receivedScheme == "" { + return nil } - if spec.url.Scheme == "" { - return true + if mockScheme == "" { + return nil } - return r.URL.Scheme == spec.url.Scheme + return errorOrNil(receivedScheme == mockScheme, func() string { + return fmt.Sprintf("received scheme %s did not match mock scheme %s", receivedScheme, mockScheme) + }) } -var headerMatcher = func(req *http.Request, spec *MockRequest) bool { - for key, values := range spec.headers { +var headerMatcher = func(req *http.Request, spec *MockRequest) error { + mockHeaders := spec.headers + for key, values := range mockHeaders { var match bool var err error - for _, field := range req.Header[key] { + receivedHeaders := req.Header + for _, field := range receivedHeaders[key] { for _, value := range values { match, err = regexp.MatchString(value, field) if err != nil { - return false + return fmt.Errorf("unable to match received header value %s against expected value %s", value, field) } } @@ -395,22 +451,25 @@ var headerMatcher = func(req *http.Request, spec *MockRequest) bool { } if !match { - return false + return fmt.Errorf("not all of received headers %s matched expected mock headers %s", receivedHeaders, mockHeaders) } } - return true + return nil } -var queryParamMatcher = func(req *http.Request, spec *MockRequest) bool { - for key, values := range spec.query { +var queryParamMatcher = func(req *http.Request, spec *MockRequest) error { + mockQueryParams := spec.query + for key, values := range mockQueryParams { var err error var match bool - for _, field := range req.URL.Query()[key] { + receivedQueryParams := req.URL.Query() + + for _, field := range receivedQueryParams[key] { for _, value := range values { match, err = regexp.MatchString(value, field) if err != nil { - return false + return fmt.Errorf("unable to match received query param value %s against expected value %s", value, field) } } @@ -420,24 +479,26 @@ var queryParamMatcher = func(req *http.Request, spec *MockRequest) bool { } if !match { - return false + return fmt.Errorf("not all of received query params %s matched expected mock query params %s", receivedQueryParams, mockQueryParams) } } - return true + return nil } -var queryPresentMatcher = func(req *http.Request, spec *MockRequest) bool { +var queryPresentMatcher = func(req *http.Request, spec *MockRequest) error { for _, query := range spec.queryPresent { if req.URL.Query().Get(query) == "" { - return false + return fmt.Errorf("expected query param %s not received", query) } } - return true + return nil } -var bodyMatcher = func(req *http.Request, spec *MockRequest) bool { - if len(spec.body) == 0 { - return true +var bodyMatcher = func(req *http.Request, spec *MockRequest) error { + mockBody := spec.body + + if len(mockBody) == 0 { + return nil } body, err := ioutil.ReadAll(req.Body) @@ -445,7 +506,7 @@ var bodyMatcher = func(req *http.Request, spec *MockRequest) bool { panic(err) } if len(body) == 0 { - return false + return errors.New("expected a body but received none") } // replace body so it can be read again @@ -453,14 +514,14 @@ var bodyMatcher = func(req *http.Request, spec *MockRequest) bool { // Perform exact string match bodyStr := string(body) - if bodyStr == spec.body { - return true + if bodyStr == mockBody { + return nil } // Perform regexp match - match, _ := regexp.MatchString(spec.body, bodyStr) + match, _ := regexp.MatchString(mockBody, bodyStr) if match == true { - return true + return nil } // Perform JSON match @@ -468,14 +529,21 @@ var bodyMatcher = func(req *http.Request, spec *MockRequest) bool { reqJSONErr := json.Unmarshal(body, &reqJSON) var matchJSON map[string]interface{} - specJSONErr := json.Unmarshal([]byte(spec.body), &matchJSON) + specJSONErr := json.Unmarshal([]byte(mockBody), &matchJSON) isJSON := reqJSONErr == nil && specJSONErr == nil if isJSON && reflect.DeepEqual(reqJSON, matchJSON) { - return true + return nil } - return false + return fmt.Errorf("received body %s did not match expected mock body %s", bodyStr, mockBody) +} + +func errorOrNil(statement bool, errorMessage func() string) error { + if statement { + return nil + } + return errors.New(errorMessage()) } var matchers = []Matcher{ diff --git a/mocks_test.go b/mocks_test.go index 33381f8..89cd441 100644 --- a/mocks_test.go +++ b/mocks_test.go @@ -2,51 +2,72 @@ package apitest import ( "encoding/json" + "errors" "fmt" "github.com/steinfletcher/apitest/assert" "io/ioutil" "net/http" "net/http/httptest" + "reflect" "strings" "testing" ) func TestMocks_QueryPresent(t *testing.T) { tests := []struct { - requestUrl string - queryParam string - isPresent bool + requestUrl string + queryParam string + expectedError error }{ - {"http://test.com/v1/path?a=1", "a", true}, - {"http://test.com/v1/path", "a", false}, - {"http://test.com/v1/path?b=1", "a", false}, - {"http://test.com/v2/path?b=2&a=1", "a", true}, + {"http://test.com/v1/path?a=1", "a", nil}, + {"http://test.com/v1/path", "a", errors.New("expected query param a not received")}, + {"http://test.com/v1/path?c=1", "b", errors.New("expected query param b not received")}, + {"http://test.com/v2/path?b=2&a=1", "a", nil}, } for _, test := range tests { t.Run(test.requestUrl, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, test.requestUrl, nil) mockRequest := NewMock().Get(test.requestUrl).QueryPresent(test.queryParam) - isPresent := queryPresentMatcher(req, mockRequest) - assert.Equal(t, test.isPresent, isPresent) + matchError := queryPresentMatcher(req, mockRequest) + assert.Equal(t, test.expectedError, matchError) }) } } +func TestMocks_NewUnmatchedMockError_Empty(t *testing.T) { + mockError := newUnmatchedMockError() + + assert.NotNil(t, mockError) + assert.Len(t, mockError.errors, 0) +} + +func TestMocks_NewEmptyUnmatchedMockError_ExpectedErrorsString(t *testing.T) { + mockError := newUnmatchedMockError(). + addErrors(1, errors.New("a boo boo has occurred")). + addErrors(2, errors.New("tom drank too much beer")) + + assert.NotNil(t, mockError) + assert.Len(t, mockError.errors, 2) + assert.Equal(t, + "received request did not match any mocks\n\nMock 1 mismatches:\n• a boo boo has occurred\n\nMock 2 mismatches:\n• tom drank too much beer\n\n", + mockError.Error()) +} + func TestMocks_HostMatcher(t *testing.T) { tests := []struct { - requestUrl string - mockUrl string - shouldMatch bool + requestUrl string + mockUrl string + expectedError error }{ - {"http://test.com", "https://test.com", true}, - {"https://test.com", "https://testa.com", false}, - {"https://test.com", "", true}, + {"http://test.com", "https://test.com", nil}, + {"https://test.com", "https://testa.com", errors.New("received host test.com did not match mock host testa.com")}, + {"https://test.com", "", nil}, } for _, test := range tests { t.Run(fmt.Sprintf("%s %s", test.requestUrl, test.mockUrl), func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, test.requestUrl, nil) - matches := hostMatcher(req, NewMock().Get(test.mockUrl)) - assert.Equal(t, test.shouldMatch, matches) + matchError := hostMatcher(req, NewMock().Get(test.mockUrl)) + assert.Equal(t, test.expectedError, matchError) }) } } @@ -56,11 +77,11 @@ func TestMocks_HeaderMatcher(t *testing.T) { requestHeaders map[string]string headerToMatchKey string headerToMatchValue string - shouldMatch bool + expectedError error }{ - {map[string]string{"B": "5", "A": "123"}, "A", "123", true}, - {map[string]string{"A": "123"}, "C", "3", false}, - {map[string]string{}, "", "", true}, + {map[string]string{"B": "5", "A": "123"}, "A", "123", nil}, + {map[string]string{"A": "123"}, "C", "3", errors.New("not all of received headers map[A:[123]] matched expected mock headers map[C:[3]]")}, + {map[string]string{}, "", "", nil}, } for _, test := range tests { t.Run(fmt.Sprintf("%s %s", test.headerToMatchKey, test.headerToMatchValue), func(t *testing.T) { @@ -72,8 +93,8 @@ func TestMocks_HeaderMatcher(t *testing.T) { if test.headerToMatchKey != "" { mockRequest.Header(test.headerToMatchKey, test.headerToMatchValue) } - matches := headerMatcher(req, mockRequest) - assert.Equal(t, test.shouldMatch, matches) + matchError := headerMatcher(req, mockRequest) + assert.Equal(t, test.expectedError, matchError) }) } } @@ -87,28 +108,28 @@ func TestMocks_MockRequest_Header_WorksWithHeaders(t *testing.T) { req.Header.Set("A", "12345") req.Header.Set("B", "67890") - matches := headerMatcher(req, mock) + matchError := headerMatcher(req, mock) - assert.Equal(t, true, matches) + assert.Nil(t, matchError) } func TestMocks_QueryMatcher(t *testing.T) { tests := []struct { - requestUrl string - queryToMatch map[string]string - shouldMatch bool + requestUrl string + queryToMatch map[string]string + expectedError error }{ - {"http://test.com/v1/path?a=1", map[string]string{"a": "1"}, true}, - {"http://test.com/v1/path", map[string]string{"a": "1"}, false}, - {"http://test.com/v2/path?a=1", map[string]string{"b": "1"}, false}, - {"http://test.com/v2/path?b=2&a=1", map[string]string{"a": "1"}, true}, + {"http://test.com/v1/path?a=1", map[string]string{"a": "1"}, nil}, + {"http://test.com/v1/path", map[string]string{"a": "1"}, errors.New("not all of received query params map[] matched expected mock query params map[a:[1]]")}, + {"http://test.com/v2/path?a=1", map[string]string{"b": "1"}, errors.New("not all of received query params map[a:[1]] matched expected mock query params map[b:[1]]")}, + {"http://test.com/v2/path?b=2&a=1", map[string]string{"a": "1"}, nil}, } for _, test := range tests { t.Run(test.requestUrl, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, test.requestUrl, nil) mockRequest := NewMock().Get(test.requestUrl).QueryParams(test.queryToMatch) - matches := queryParamMatcher(req, mockRequest) - assert.Equal(t, test.shouldMatch, matches) + matchError := queryParamMatcher(req, mockRequest) + assert.Equal(t, test.expectedError, matchError) }) } } @@ -120,30 +141,30 @@ func TestMocks_QueryParams_DoesNotOverwriteQuery(t *testing.T) { Query("b", "2"). QueryParams(map[string]string{"a": "1"}) - matches := queryParamMatcher(req, mockRequest) + matchError := queryParamMatcher(req, mockRequest) assert.Equal(t, 2, len(mockRequest.query)) - assert.Equal(t, true, matches) + assert.Nil(t, matchError) } func TestMocks_SchemeMatcher(t *testing.T) { tests := []struct { - requestUrl string - mockUrl string - shouldMatch bool + requestUrl string + mockUrl string + expectedError error }{ - {"http://test.com", "https://test.com", false}, - {"https://test.com", "https://test.com", true}, - {"https://test.com", "test.com", true}, - {"localhost:80", "localhost:80", true}, + {"http://test.com", "https://test.com", errors.New("received scheme http did not match mock scheme https")}, + {"https://test.com", "https://test.com", nil}, + {"https://test.com", "test.com", nil}, + {"localhost:80", "localhost:80", nil}, } for _, test := range tests { t.Run(fmt.Sprintf("%s %s", test.requestUrl, test.mockUrl), func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, test.requestUrl, nil) - matches := schemeMatcher(req, NewMock().Get(test.mockUrl)) - if !matches == test.shouldMatch { - t.Fatalf("mockUrl='%s' requestUrl='%s' shouldMatch=%v", - test.mockUrl, test.requestUrl, test.shouldMatch) + matchError := schemeMatcher(req, NewMock().Get(test.mockUrl)) + if !reflect.DeepEqual(matchError, test.expectedError) { + t.Fatalf("mockUrl='%s' requestUrl='%s' actual=%v shouldMatch=%v", + test.mockUrl, test.requestUrl, matchError, test.expectedError) } }) } @@ -151,51 +172,51 @@ func TestMocks_SchemeMatcher(t *testing.T) { func TestMocks_BodyMatcher(t *testing.T) { tests := []struct { - requestBody string - matchBody string - shouldMatch bool + requestBody string + matchBody string + expectedError error }{ - {`{"a": 1}`, "", true}, - {``, `{"a":1}`, false}, - {"golang\n", "go[lang]?", true}, - {"golang\n", "go[lang]?", true}, - {"golang", "goat", false}, - {"go\n", "go[lang]?", true}, - {`{"a":"12345"}\n`, `{"a":"12345"}`, true}, - {`{"a":"12345"}`, `{"b":"12345"}`, false}, - {`{"x":"12345"}`, `{"x":"12345"}`, true}, + {`{"a": 1}`, "", nil}, + {``, `{"a":1}`, errors.New("expected a body but received none")}, + {"golang\n", "go[lang]?", nil}, + {"golang\n", "go[lang]?", nil}, + {"golang", "goat", errors.New("received body golang did not match expected mock body goat")}, + {"go\n", "go[lang]?", nil}, + {`{"a":"12345"}\n`, `{"a":"12345"}`, nil}, + {`{"a":"12345"}`, `{"b":"12345"}`, errors.New(`received body {"a":"12345"} did not match expected mock body {"b":"12345"}`)}, + {`{"x":"12345"}`, `{"x":"12345"}`, nil}, {`{"a": 12345, "b": [{"key": "c", "value": "result"}]}`, - `{"b":[{"key":"c","value":"result"}],"a":12345}`, true}, + `{"b":[{"key":"c","value":"result"}],"a":12345}`, nil}, } for _, test := range tests { t.Run(fmt.Sprintf("body=%v", test.matchBody), func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/path", strings.NewReader(test.requestBody)) - matches := bodyMatcher(req, NewMock().Get("/path").Body(test.matchBody)) - assert.Equal(t, test.shouldMatch, matches) + matchError := bodyMatcher(req, NewMock().Get("/path").Body(test.matchBody)) + assert.Equal(t, test.expectedError, matchError) }) } } func TestMocks_PathMatcher(t *testing.T) { tests := []struct { - requestUrl string - pathToMatch string - shouldMatch bool + requestUrl string + pathToMatch string + expectedError error }{ - {"http://test.com/v1/path", "/v1/path", true}, - {"http://test.com/v1/path", "/v1/not", false}, - {"http://test.com/v1/path", "", true}, - {"http://test.com", "", true}, - {"http://test.com/v2/path", "/v2/.+th", true}, + {"http://test.com/v1/path", "/v1/path", nil}, + {"http://test.com/v1/path", "/v1/not", errors.New("received path /v1/path did not match mock path /v1/not")}, + {"http://test.com/v1/path", "", nil}, + {"http://test.com", "", nil}, + {"http://test.com/v2/path", "/v2/.+th", nil}, } for _, test := range tests { t.Run(test.pathToMatch, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, test.requestUrl, nil) - matches := pathMatcher(req, NewMock().Get(test.pathToMatch)) - if !matches == test.shouldMatch { + matchError := pathMatcher(req, NewMock().Get(test.pathToMatch)) + if matchError != nil && !reflect.DeepEqual(matchError, test.expectedError) { t.Fatalf("methodToMatch='%s' requestUrl='%s' shouldMatch=%v", - test.pathToMatch, test.requestUrl, test.shouldMatch) + test.pathToMatch, test.requestUrl, matchError) } }) } @@ -213,6 +234,7 @@ func TestMocks_PanicsIfUrlInvalid(t *testing.T) { func TestMocks_Matches(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/preferences/12345", nil) + getPreferences := NewMock(). Get("/preferences/12345"). RespondWith(). @@ -226,8 +248,9 @@ func TestMocks_Matches(t *testing.T) { Body(`{"name": "jon", "id": "1234"}`). End() - mockResponse := matches(req, []*Mock{getUser, getPreferences}) + mockResponse, matchErrors := matches(req, []*Mock{getUser, getPreferences}) + assert.Nil(t, matchErrors) assert.NotNil(t, mockResponse) assert.Equal(t, `{"is_contactable": true}`, mockResponse.body) } @@ -235,30 +258,67 @@ func TestMocks_Matches(t *testing.T) { func TestMocks_Matches_NilIfNoMatch(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/preferences/12345", nil) - mockResponse := matches(req, []*Mock{}) + mockResponse, matchErrors := matches(req, []*Mock{}) + + if mockResponse != nil { + t.Fatal("Expected nil") + } + + assert.NotNil(t, matchErrors) + assert.Equal(t, newUnmatchedMockError(), matchErrors) +} + +func TestMocks_UnmatchedMockErrorOrderedMockKeys(t *testing.T) { + unmatchedMockError := newUnmatchedMockError(). + addErrors(3, errors.New("oh no")). + addErrors(1, errors.New("oh shoot")). + addErrors(4, errors.New("gah")) + + assert.Equal(t, + "received request did not match any mocks\n\nMock 1 mismatches:\n• oh shoot\n\nMock 3 mismatches:\n• oh no\n\nMock 4 mismatches:\n• gah\n\n", + unmatchedMockError.Error()) +} + +func TestMocks_Matches_ErrorsMatchUnmatchedMocks(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/preferences/12345", nil) + + mockResponse, matchErrors := matches(req, + []*Mock{ + NewMock(). + Get("/preferences/123456"). + RespondWith(). + End()}) if mockResponse != nil { - t.Fatal("expected mockResponse to be nil") + t.Fatal("Expected nil") } + + assert.NotNil(t, matchErrors) + assert.Equal(t, "received request did not match any mocks\n\nMock 1 mismatches:\n• received path /preferences/12345 did not match mock path /preferences/123456\n\n", + matchErrors.Error()) } func TestMocks_MethodMatcher(t *testing.T) { tests := []struct { requestMethod string methodToMatch string - shouldMatch bool + expectedError error }{ - {http.MethodGet, http.MethodGet, true}, - {http.MethodDelete, "", true}, - {http.MethodPut, http.MethodGet, false}, + {http.MethodGet, http.MethodGet, nil}, + {http.MethodPost, http.MethodPost, nil}, + {http.MethodDelete, "", nil}, + {http.MethodPut, http.MethodGet, errors.New("received method PUT did not match mock method GET")}, + {"", http.MethodGet, nil}, + {"", "", nil}, + {http.MethodOptions, http.MethodGet, errors.New("received method OPTIONS did not match mock method GET")}, } for _, test := range tests { t.Run(test.requestMethod, func(t *testing.T) { req := httptest.NewRequest(test.requestMethod, "/path", nil) - matches := methodMatcher(req, NewMock().Method(test.methodToMatch)) - if !matches == test.shouldMatch { - t.Fatalf("methodToMatch='%s' requestMethod='%s' shouldMatch=%v", - test.methodToMatch, test.requestMethod, test.shouldMatch) + matchError := methodMatcher(req, NewMock().Method(test.methodToMatch)) + if !reflect.DeepEqual(matchError, test.expectedError) { + t.Fatalf("methodToMatch='%s' requestMethod='%s' actual=%v shouldMatch=%v", + test.methodToMatch, test.requestMethod, matchError, test.expectedError) } }) }