├── defaults.go ├── defaults_go15.go ├── .travis.yml ├── .gitignore ├── LICENSE ├── httpmock.go ├── mock_builder.go ├── httpmock_test.go └── README.md /defaults.go: -------------------------------------------------------------------------------- 1 | // +build !go1.5 2 | 3 | package httpmock 4 | 5 | const defaultUserAgent = "Go 1.1 package http" 6 | -------------------------------------------------------------------------------- /defaults_go15.go: -------------------------------------------------------------------------------- 1 | // +build go1.5 2 | 3 | package httpmock 4 | 5 | const defaultUserAgent = "Go-http-client/1.1" 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.3 5 | - 1.4 6 | - 1.5beta1 7 | - tip 8 | script: 9 | - go test -v ./ 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Golang libraries for everyone 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 | 23 | -------------------------------------------------------------------------------- /httpmock.go: -------------------------------------------------------------------------------- 1 | package httpmock 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | ) 7 | 8 | type MockResponse struct { 9 | Request http.Request 10 | Response Response 11 | } 12 | 13 | type MockHTTPServer struct { 14 | Listener net.Listener 15 | ResponseMap map[string]Response 16 | } 17 | 18 | type Response struct { 19 | StatusCode int 20 | Header http.Header 21 | Body string 22 | } 23 | 24 | func NewMockHTTPServer(b ...string) *MockHTTPServer { 25 | var err error 26 | m := &MockHTTPServer{ 27 | ResponseMap: make(map[string]Response), 28 | } 29 | if len(b) == 0 { 30 | m.Listener, err = net.Listen("tcp", ":9001") 31 | } else { 32 | m.Listener, err = net.Listen("tcp", b[0]) 33 | } 34 | 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | go http.Serve(m.Listener, m) 40 | return m 41 | } 42 | 43 | func (m *MockHTTPServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { 44 | reqString, err := request2string(*req) 45 | if err != nil { 46 | w.WriteHeader(400) 47 | w.Write([]byte("invalid request")) 48 | } else { 49 | resp, ok := m.ResponseMap[reqString] 50 | if ok { 51 | if resp.Header != nil { 52 | h := w.Header() 53 | for k, v := range resp.Header { 54 | for _, val := range v { 55 | h.Add(k, val) 56 | } 57 | } 58 | } 59 | if resp.StatusCode != 0 { 60 | w.WriteHeader(resp.StatusCode) 61 | } 62 | w.Write([]byte(resp.Body)) 63 | } else { 64 | w.WriteHeader(404) 65 | w.Write([]byte("route not mocked")) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /mock_builder.go: -------------------------------------------------------------------------------- 1 | package httpmock 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | func (m *MockHTTPServer) AddResponse(resp MockResponse) error { 12 | requestString, err := request2string(resp.Request) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | m.ResponseMap[requestString] = resp.Response 18 | 19 | return nil 20 | } 21 | 22 | func (m *MockHTTPServer) AddResponses(resp []MockResponse) error { 23 | for _, r := range resp { 24 | if err := m.AddResponse(r); err != nil { 25 | return err 26 | } 27 | } 28 | return nil 29 | } 30 | 31 | func request2string(req http.Request) (string, error) { 32 | addRequestDefaults(&req) 33 | fragments := []string{ 34 | req.Method, 35 | req.URL.RequestURI(), 36 | } 37 | if req.Body != nil { 38 | if body, err := ioutil.ReadAll(req.Body); err != nil { 39 | return "", err 40 | } else { 41 | fragments = append(fragments, string(body)) 42 | } 43 | } else { 44 | fragments = append(fragments, "") 45 | } 46 | 47 | headerStrings := make([]string, 0) 48 | for index, values := range req.Header { 49 | headerStrings = append(headerStrings, fmt.Sprintf("%s: %s", strings.ToLower(index), strings.Join(values, ","))) 50 | } 51 | sort.Strings(headerStrings) 52 | fragments = append(fragments, headerStrings...) 53 | 54 | return strings.Join(fragments, "|"), nil 55 | } 56 | 57 | func addRequestDefaults(req *http.Request) { 58 | if req.Header == nil { 59 | req.Header = http.Header{} 60 | } 61 | if req.Header.Get("User-Agent") == "" { 62 | req.Header.Set("User-Agent", defaultUserAgent) 63 | } 64 | 65 | if req.Header.Get("Accept-Encoding") == "" { 66 | req.Header.Set("Accept-Encoding", "gzip") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /httpmock_test.go: -------------------------------------------------------------------------------- 1 | package httpmock 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/url" 7 | "testing" 8 | ) 9 | 10 | func TestBasicResponse(t *testing.T) { 11 | mockServer := NewMockHTTPServer() 12 | u, _ := url.Parse("http://127.0.0.1:9001/54") 13 | badU, _ := url.Parse("http://127.0.0.1:9001/10000") 14 | mockServer.AddResponses([]MockResponse{ 15 | { 16 | Request: http.Request{ 17 | Method: "GET", 18 | URL: u, 19 | }, 20 | Response: Response{ 21 | StatusCode: 200, 22 | Body: "it's alive!", 23 | }, 24 | }, 25 | }) 26 | 27 | checks := []struct { 28 | in http.Request 29 | out Response 30 | }{ 31 | { 32 | in: http.Request{ 33 | Method: "GET", 34 | URL: u, 35 | }, 36 | out: Response{ 37 | StatusCode: 200, 38 | Body: "it's alive!", 39 | }, 40 | }, 41 | { 42 | in: http.Request{ 43 | Method: "POST", 44 | URL: u, 45 | }, 46 | out: Response{ 47 | StatusCode: 404, 48 | Body: "route not mocked", 49 | }, 50 | }, 51 | { 52 | in: http.Request{ 53 | Method: "GET", 54 | URL: badU, 55 | }, 56 | out: Response{ 57 | StatusCode: 404, 58 | Body: "route not mocked", 59 | }, 60 | }, 61 | } 62 | 63 | client := &http.Client{} 64 | 65 | for _, tt := range checks { 66 | resp, err := client.Do(&tt.in) 67 | if err != nil { 68 | t.FailNow() 69 | } 70 | if resp.StatusCode != tt.out.StatusCode { 71 | t.Errorf("Expected status code %d, received %d", tt.out.StatusCode, resp.StatusCode) 72 | } 73 | 74 | if resp.Body != nil { 75 | if body, err := ioutil.ReadAll(resp.Body); err != nil { 76 | t.Errorf("Response body read error") 77 | } else { 78 | if string(body) != tt.out.Body { 79 | t.Errorf("Expected response: `%s`, received:`%s`", tt.out.Body, string(body)) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/goware/httpmock.svg?branch=master)](https://travis-ci.org/goware/httpmock) 2 | 3 | # httpmock 4 | Mocking 3rd party services in Go made simple 5 | 6 | 7 | ### How does it work 8 | Httpmock runs a local server (within test) that serves predefined responses to http requests effectively faking 3rd party service in a fast, reliable way that doesn't require you to change your code, only settings (unless of course you've hardcoded service urls - that you'll have to change). 9 | 10 | ### Example 11 | ```go 12 | package yours 13 | 14 | import ( 15 | "testing" 16 | "net/http" 17 | "io/ioutil" 18 | "github.com/goware/httpmock" 19 | ) 20 | 21 | 22 | // the code you want to test 23 | func Access3rdPartyService() (http.Response, error) { 24 | return http.Get(serviceYouDontControl) 25 | } 26 | 27 | // normally http://example.com/api, but we're changing it to use mock server 28 | // this should be the only change necessary to run tests 29 | var serviceYouDontControl = "http://127.0.0.1:10000/api/list" 30 | 31 | func TestSomething(t *testing.T) { 32 | 33 | // new mocking server 34 | mockService := httpmock.NewMockHTTPServer("127.0.0.1:10000") 35 | 36 | // define request->response pairs 37 | requestUrl, _ := url.Parse("http://127.0.0.1:10000/api/list") 38 | mockService.AddResponses([]httpmock.MockResponse{ 39 | { 40 | Request: http.Request{ 41 | Method: "GET", 42 | URL: requestUrl, 43 | }, 44 | Response: httpmock.Response{ 45 | StatusCode: 200, 46 | Body: "it's alive!", 47 | }, 48 | }, 49 | }) 50 | 51 | // test code relying on 3rd party service 52 | serviceResponse, _ := Access3rdPartyService() 53 | if serviceResponse.StatusCode != 200 { 54 | t.Errorf("Expected status code %d, received %d", 200, serviceResponse.StatusCode) 55 | } 56 | if body, err := ioutil.ReadAll(serviceResponse.Body); err != nil { 57 | t.Errorf("Response body read error") 58 | } else { 59 | if string(body) != "it's alive!" { 60 | t.Errorf("Expected response: `%s`, received:`%s`", "it's alive!", string(body)) 61 | } 62 | } 63 | } 64 | ``` 65 | --------------------------------------------------------------------------------