├── .travis.yml ├── okhandler.go ├── go.mod ├── .gitignore ├── LICENSE ├── go.sum ├── httpmock_test.go ├── mockhandler.go ├── README.md └── httpmock.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "tip" 4 | -------------------------------------------------------------------------------- /okhandler.go: -------------------------------------------------------------------------------- 1 | package httpmock 2 | 3 | // OKHandler is a simple Handler that returns 200 OK responses for any request. 4 | type OKHandler struct { 5 | } 6 | 7 | // Handle makes this implement the Handler interface. 8 | func (r *OKHandler) Handle(method, path string, body []byte) Response { 9 | return Response{Status: 200} 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dankinder/httpmock 2 | 3 | go 1.19 4 | 5 | require github.com/stretchr/testify v1.8.4 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | github.com/stretchr/objx v0.5.1 // indirect 11 | gopkg.in/yaml.v3 v3.0.1 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 dankinder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= 10 | github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= 11 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 13 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 14 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 15 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /httpmock_test.go: -------------------------------------------------------------------------------- 1 | package httpmock 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestBasicRequestResponse(t *testing.T) { 15 | 16 | downstream := NewMockHandler(t) 17 | 18 | downstream.On("Handle", "GET", "/object/12345", mock.Anything).Return(Response{ 19 | Body: []byte(`{"status": "ok"}`), 20 | }) 21 | 22 | s := NewServer(downstream) 23 | defer s.Close() 24 | 25 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/object/12345", s.URL()), nil) 26 | require.NoError(t, err) 27 | 28 | resp, err := http.DefaultClient.Do(req) 29 | require.NoError(t, err) 30 | 31 | body, err := io.ReadAll(resp.Body) 32 | require.NoError(t, err) 33 | assert.Equal(t, []byte(`{"status": "ok"}`), body) 34 | 35 | downstream.AssertExpectations(t) 36 | } 37 | 38 | func TestBasicRequestResponseWithHeaders(t *testing.T) { 39 | headerKey := "HTTPMOCK-TEST" 40 | headerVal := "its here" 41 | downstream := NewMockHandlerWithHeaders(t) 42 | 43 | downstream.On( 44 | "HandleWithHeaders", 45 | "GET", 46 | "/object/12345", 47 | HeaderMatcher(headerKey, headerVal), 48 | mock.Anything, 49 | ). 50 | Return(Response{ 51 | Body: []byte(`{"status": "ok"}`), 52 | }) 53 | 54 | s := NewServer(downstream) 55 | defer s.Close() 56 | 57 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/object/12345", s.URL()), nil) 58 | require.NoError(t, err) 59 | 60 | req.Header.Set(headerKey, headerVal) 61 | resp, err := http.DefaultClient.Do(req) 62 | require.NoError(t, err) 63 | 64 | body, err := io.ReadAll(resp.Body) 65 | require.NoError(t, err) 66 | 67 | assert.Equal(t, []byte(`{"status": "ok"}`), body) 68 | 69 | downstream.AssertExpectations(t) 70 | } 71 | 72 | func TestMultiHeaderMatcher(t *testing.T) { 73 | headerKey := "HTTPMOCK-TEST" 74 | headerVal := "its here" 75 | headerKey2 := "HTTPMOCK-TEST-2" 76 | headerVal2 := "its here too!" 77 | downstream := NewMockHandlerWithHeaders(t) 78 | 79 | downstream.On( 80 | "HandleWithHeaders", 81 | "GET", 82 | "/object/12345", 83 | MultiHeaderMatcher(http.Header{ 84 | headerKey: []string{headerVal}, 85 | headerKey2: []string{headerVal2}, 86 | }), 87 | mock.Anything, 88 | ). 89 | Return(Response{ 90 | Body: []byte(`{"status": "ok"}`), 91 | }) 92 | 93 | s := NewServer(downstream) 94 | defer s.Close() 95 | 96 | req, err := http.NewRequest("GET", fmt.Sprintf("%s/object/12345", s.URL()), nil) 97 | require.NoError(t, err) 98 | 99 | req.Header.Set(headerKey, headerVal) 100 | req.Header.Set(headerKey2, headerVal2) 101 | _, err = http.DefaultClient.Do(req) 102 | require.NoError(t, err) 103 | 104 | downstream.AssertExpectations(t) 105 | } 106 | -------------------------------------------------------------------------------- /mockhandler.go: -------------------------------------------------------------------------------- 1 | package httpmock 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | 9 | "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // MockHandler is a httpmock.Handler that uses github.com/stretchr/testify/mock. 13 | type MockHandler struct { 14 | mock.Mock 15 | } 16 | 17 | // Handle makes this implement the Handler interface. 18 | func (m *MockHandler) Handle(method, path string, body []byte) Response { 19 | args := m.Called(method, path, body) 20 | return args.Get(0).(Response) 21 | } 22 | 23 | // MockHandlerWithHeaders is a httpmock.Handler that uses github.com/stretchr/testify/mock. 24 | type MockHandlerWithHeaders struct { 25 | mock.Mock 26 | } 27 | 28 | // Handle makes this implement the Handler interface. 29 | func (m *MockHandlerWithHeaders) Handle(method, path string, body []byte) Response { 30 | args := m.Called(method, path, body) 31 | return args.Get(0).(Response) 32 | } 33 | 34 | // HandleWithHeaders makes this implement the HandlerWithHeaders interface. 35 | func (m *MockHandlerWithHeaders) HandleWithHeaders(method, path string, headers http.Header, body []byte) Response { 36 | args := m.Called(method, path, headers, body) 37 | return args.Get(0).(Response) 38 | } 39 | 40 | // JSONMatcher returns a mock.MatchedBy func to check if the argument is the json form of the provided object. 41 | // See the github.com/stretchr/testify/mock documentation and example in httpmock.go. 42 | func JSONMatcher(o1 interface{}) interface{} { 43 | return mock.MatchedBy(func(arg []byte) bool { 44 | // Just using reflect.New on the TypeOf(o1) does not work here, since o1 is an interface. We have to grab the 45 | // underlying type (Indirect) and create a pointer to that type instead. If you do it the former way, the values 46 | // LOOK equal, but DeepEqual will always return false, since the pointer type is different. 47 | o2 := reflect.New(reflect.Indirect(reflect.ValueOf(o1)).Type()).Interface() 48 | err := json.Unmarshal(arg, o2) 49 | if err != nil { 50 | // Assume that this call doesn't match us since we couldn't parse the json 51 | return false 52 | } 53 | return reflect.DeepEqual(o1, o2) 54 | }) 55 | } 56 | 57 | // ToJSON is a convenience function for converting an object to JSON inline. It panics on failure, so should be used 58 | // only in test code. 59 | func ToJSON(obj interface{}) []byte { 60 | data, err := json.Marshal(obj) 61 | if err != nil { 62 | panic(fmt.Sprintf("failed to marshal object %v: %v", obj, err)) 63 | } 64 | return data 65 | } 66 | 67 | // HeaderMatcher matches the presence of a header named key that has a given value. Other headers 68 | // are allowed to exist and are not checked. 69 | func HeaderMatcher(key, value string) interface{} { 70 | headers := make(http.Header) 71 | headers.Set(key, value) 72 | return MultiHeaderMatcher(headers) 73 | } 74 | 75 | // MultiHeaderMatcher matches the presence and content of multiple headers. Other headers besides those 76 | // within desiredHeaders are allowed to exist and are not checked. 77 | func MultiHeaderMatcher(desiredHeaders http.Header) interface{} { 78 | return mock.MatchedBy(func(headers http.Header) bool { 79 | for key, val := range desiredHeaders { 80 | if headers.Get(key) != val[0] { 81 | return false 82 | } 83 | } 84 | return true 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httpmock 2 | 3 | GoDoc 4 | Go Report Card 5 | Build Status 6 | 7 | This library builds on Go's built-in [httptest](https://golang.org/pkg/net/http/httptest/) library, adding a more 8 | mockable interface that can be used easily with other mocking tools like 9 | [testify/mock](https://godoc.org/github.com/stretchr/testify/mock). It does this by providing a Handler that receives 10 | HTTP components as separate arguments rather than a single `*http.Request` object. 11 | 12 | Where the typical [http.Handler](https://golang.org/pkg/net/http/#Handler) interface is: 13 | ```go 14 | type Handler interface { 15 | ServeHTTP(ResponseWriter, *Request) 16 | } 17 | ``` 18 | This library provides a server with the following interface, which works naturally with mocking libraries: 19 | ```go 20 | // Handler is the interface used by httpmock instead of http.Handler so that it can be mocked very easily. 21 | type Handler interface { 22 | Handle(method, path string, body []byte) Response 23 | } 24 | ``` 25 | 26 | ## Examples 27 | 28 | The most primitive example, the `OKHandler`, just returns `200 OK` to everything. 29 | ```go 30 | s := httpmock.NewServer(&httpmock.OKHandler{}) 31 | defer s.Close() 32 | 33 | // Make any requests you want to s.URL(), using it as the mock downstream server 34 | ``` 35 | 36 | This example uses MockHandler, a Handler that is a [testify/mock](https://godoc.org/github.com/stretchr/testify/mock) 37 | object. 38 | 39 | ```go 40 | downstream := httpmock.NewMockHandler(t) 41 | 42 | // A simple GET that returns some pre-canned content 43 | downstream.On("Handle", "GET", "/object/12345", mock.Anything).Return(httpmock.Response{ 44 | Body: []byte(`{"status": "ok"}`), 45 | }) 46 | 47 | s := httpmock.NewServer(downstream) 48 | defer s.Close() 49 | 50 | // 51 | // Make any requests you want to s.URL(), using it as the mock downstream server 52 | // 53 | 54 | downstream.AssertExpectations(t) 55 | ``` 56 | 57 | If instead you wish to match against headers as well, a slightly different httpmock object can be used (please note the change in function name to be matched against): 58 | 59 | ```go 60 | downstream := &httpmock.NewMockHandlerWithHeaders(t) 61 | 62 | // A simple GET that returns some pre-canned content and expects a specific header. 63 | // Use MultiHeaderMatcher for multiple headers. 64 | downstream.On("HandleWithHeaders", "GET", "/object/12345", HeaderMatcher("MOCK", "this"), mock.Anything).Return(httpmock.Response{ 65 | Body: []byte(`{"status": "ok"}`), 66 | }) 67 | 68 | // ... same as above 69 | 70 | ``` 71 | 72 | The httpmock package also provides helpers for checking calls using json objects, like so: 73 | 74 | ```go 75 | // This tests a hypothetical "echo" endpoint, which returns the body we pass to it. 76 | type Obj struct { 77 | A string `json:"a"` 78 | } 79 | 80 | o := &Obj{A: "aye"} 81 | 82 | // JSONMatcher ensures that this mock is triggered only when the HTTP body, when deserialized, matches the given 83 | // object. Here, this mock response will get triggered only if `{"a":"aye"}` is sent. 84 | downstream.On("Handle", "POST", "/echo", httpmock.JSONMatcher(o)).Return(httpmock.Response{ 85 | Body: httpmock.ToJSON(o), 86 | }) 87 | ``` 88 | -------------------------------------------------------------------------------- /httpmock.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package httpmock builds on httptest, providing easier API mocking. 3 | 4 | Essentially all httpmock does is implement a similar interface to httptest, but using a Handler that receives the HTTP 5 | method, path, and body rather than a request object. This makes it very easy to use a featureful mock as the handler, 6 | e.g. github.com/stretchr/testify/mock 7 | 8 | Examples 9 | 10 | s := httpmock.NewServer(&httpmock.OKHandler{}) 11 | defer s.Close() 12 | 13 | // Make any requests you want to s.URL(), using it as the mock downstream server 14 | 15 | This example uses MockHandler, a Handler that is a github.com/stretchr/testify/mock object. 16 | 17 | downstream := httpmock.NewMockHandler(t) 18 | 19 | // A simple GET that returns some pre-canned content 20 | downstream.On("Handle", "GET", "/object/12345", mock.Anything).Return(httpmock.Response{ 21 | Body: []byte(`{"status": "ok"}`), 22 | }) 23 | 24 | s := httpmock.NewServer(downstream) 25 | defer s.Close() 26 | 27 | // 28 | // Make any requests you want to s.URL(), using it as the mock downstream server 29 | // 30 | 31 | downstream.AssertExpectations(t) 32 | 33 | If instead you wish to match against headers as well, a slightly different httpmock object can be used 34 | (please note the change in function name to be matched against): 35 | 36 | downstream := httpmock.NewMockHandlerWithHeaders(t) 37 | 38 | // A simple GET that returns some pre-canned content 39 | downstream.On("HandleWithHeaders", "GET", "/object/12345", MatchHeader("MOCK", "this"), mock.Anything).Return(httpmock.Response{ 40 | Body: []byte(`{"status": "ok"}`), 41 | }) 42 | 43 | // ... same as above 44 | 45 | Httpmock also provides helpers for checking calls using json objects, like so: 46 | 47 | // This tests a hypothetical "echo" endpoint, which returns the body we pass to it. 48 | type Obj struct { 49 | A string `json:"a"` 50 | B string `json:"b"` 51 | } 52 | 53 | o := &Obj{A: "ay", B: "bee"} 54 | 55 | // JSONMatcher ensures that this mock is triggered only when the HTTP body, when deserialized, matches the given 56 | // object. 57 | downstream.On("Handle", "POST", "/echo", httpmock.JSONMatcher(o)).Return(httpmock.Response{ 58 | Body: httpmock.ToJSON(o), 59 | }) 60 | */ 61 | package httpmock 62 | 63 | import ( 64 | "io" 65 | "log" 66 | "net/http" 67 | "net/http/httptest" 68 | "testing" 69 | ) 70 | 71 | // Handler is the interface used by httpmock instead of http.Handler so that it can be mocked very easily. 72 | type Handler interface { 73 | Handle(method, path string, body []byte) Response 74 | } 75 | 76 | // HandlerWithHeaders is the interface used by httpmock instead of http.Handler so that it can be mocked very easily, 77 | // it additionally allows matching on headers. 78 | type HandlerWithHeaders interface { 79 | Handler 80 | HandleWithHeaders(method, path string, headers http.Header, body []byte) Response 81 | } 82 | 83 | // NewMockHandler returns a pointer to a new mock handler with the test struct set 84 | func NewMockHandler(t *testing.T) *MockHandler { 85 | handler := &MockHandler{} 86 | handler.Test(t) 87 | return handler 88 | } 89 | 90 | // NewMockHandlerWithHeaders returns a pointer to a new mock handler with headers with the test struct set 91 | func NewMockHandlerWithHeaders(t *testing.T) *MockHandlerWithHeaders { 92 | handler := &MockHandlerWithHeaders{} 93 | handler.Test(t) 94 | return handler 95 | } 96 | 97 | // Response holds the response a handler wants to return to the client. 98 | type Response struct { 99 | // The HTTP status code to write (default: 200) 100 | Status int 101 | // Headers to add to the response 102 | Header http.Header 103 | // The response body to write (default: no body) 104 | Body []byte 105 | } 106 | 107 | // Server listens for requests and interprets them into calls to your Handler. 108 | type Server struct { 109 | httpServer *httptest.Server 110 | } 111 | 112 | // NewServer constructs a new server and starts it (compare to httptest.NewServer). It needs to be Closed()ed. 113 | // If you pass a handler that conforms to the HandlerWithHeaders interface, when requests are received, the 114 | // HandleWithHeaders method will be called rather than Handle. 115 | func NewServer(handler Handler) *Server { 116 | s := NewUnstartedServer(handler) 117 | s.Start() 118 | return s 119 | } 120 | 121 | // NewUnstartedServer constructs a new server but doesn't start it (compare to httptest.NewUnstartedServer). 122 | // If you pass a handler that conforms to the HandlerWithHeaders interface, when requests are received, the 123 | // HandleWithHeaders method will be called rather than Handle. 124 | func NewUnstartedServer(handler Handler) *Server { 125 | converter := &httpToHTTPMockHandler{} 126 | if hh, ok := handler.(HandlerWithHeaders); ok { 127 | converter.handlerWithHeaders = hh 128 | } else { 129 | converter.handler = handler 130 | } 131 | s := &Server{ 132 | httpServer: httptest.NewUnstartedServer(converter), 133 | } 134 | 135 | return s 136 | } 137 | 138 | // Start starts an unstarted server. 139 | func (s *Server) Start() { 140 | s.httpServer.Start() 141 | } 142 | 143 | // Close shuts down a started server. 144 | func (s *Server) Close() { 145 | s.httpServer.Close() 146 | } 147 | 148 | // URL is the URL for the local test server, i.e. the value of httptest.Server.URL 149 | func (s *Server) URL() string { 150 | return s.httpServer.URL 151 | } 152 | 153 | // httpToHTTPMockHandler is a normal http.Handler that converts the request into a httpmock.Handler call and calls the 154 | // httmock handler. 155 | type httpToHTTPMockHandler struct { 156 | handler Handler 157 | handlerWithHeaders HandlerWithHeaders 158 | } 159 | 160 | // ServeHTTP makes this implement http.Handler 161 | func (h *httpToHTTPMockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 162 | body, err := io.ReadAll(r.Body) 163 | if err != nil { 164 | log.Printf("Failed to read HTTP body in httpmock: %v", err) 165 | } 166 | var resp Response 167 | if h.handler != nil { 168 | resp = h.handler.Handle(r.Method, r.URL.RequestURI(), body) 169 | } else { 170 | resp = h.handlerWithHeaders.HandleWithHeaders(r.Method, r.URL.RequestURI(), r.Header, body) 171 | } 172 | 173 | for k, v := range resp.Header { 174 | for _, val := range v { 175 | w.Header().Add(k, val) 176 | } 177 | } 178 | 179 | status := resp.Status 180 | if status == 0 { 181 | status = 200 182 | } 183 | w.WriteHeader(status) 184 | _, err = w.Write(resp.Body) 185 | if err != nil { 186 | log.Printf("Failed to write response in httpmock: %v", err) 187 | } 188 | } 189 | --------------------------------------------------------------------------------