├── .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 |
4 |
5 |
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 |
--------------------------------------------------------------------------------