├── .gitignore ├── LICENSE ├── proxy.go ├── proxy_test.go ├── switcher.go ├── doc.go ├── switcher_test.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # OS-specific 2 | *~ 3 | *# 4 | 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Jo Chasinga 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Jo Chasinga. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package relay 6 | 7 | import ( 8 | "net/http" 9 | "net/http/httptest" 10 | "time" 11 | ) 12 | 13 | // HTTPTestServer is an interface which all instances including httptest.Server implement. 14 | type HTTPTestServer interface { 15 | Close() 16 | CloseClientConnections() 17 | Start() 18 | StartTLS() 19 | } 20 | 21 | // A Proxy is an HTTPTestServer placed in front of another 22 | // HTTPTestServer to simulate a real proxy server or a connection with latency. 23 | type Proxy struct { 24 | *httptest.Server 25 | Latency time.Duration 26 | Backend HTTPTestServer 27 | } 28 | 29 | // Close shuts down the proxy and blocks until all outstanding requests 30 | // on this proxy have completed. 31 | func (p *Proxy) Close() { 32 | p.Server.Close() 33 | } 34 | 35 | // Start starts a proxy from NewUnstartedProxy. 36 | func (p *Proxy) Start() { 37 | p.Server.Start() 38 | } 39 | 40 | // TODO: Write test for SetPort(). 41 | // SetPort optionally sets a local port number a Proxy should listen on. 42 | // It should be set on an unstarted proxy only. 43 | /* 44 | func (p *Proxy) setPort(port string) { 45 | l, err := net.Listen("tcp", "127.0.0.1:" + port) 46 | if err != nil { 47 | if l, err = net.Listen("tcp6", "[::1]:" + port); err != nil { 48 | panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) 49 | } 50 | } 51 | p.Server.Listener = l 52 | } 53 | */ 54 | 55 | // NewUnstartedProxy Start an unstarted proxy instance. 56 | func NewUnstartedProxy(latency time.Duration, backend HTTPTestServer) *Proxy { 57 | middleFunc := func(w http.ResponseWriter, r *http.Request) { 58 | <-time.After(latency) 59 | func(w http.ResponseWriter, r *http.Request) { 60 | <-time.After(latency) 61 | s, ok := backend.(*httptest.Server) 62 | if ok { 63 | s.Config.Handler.ServeHTTP(w, r) 64 | } else { 65 | p := backend.(*Proxy) 66 | p.Config.Handler.ServeHTTP(w, r) 67 | } 68 | }(w, r) 69 | } 70 | middleServer := httptest.NewUnstartedServer(http.HandlerFunc(middleFunc)) 71 | proxy := &Proxy{ 72 | Server: middleServer, 73 | Latency: latency, 74 | Backend: backend, 75 | } 76 | return proxy 77 | } 78 | 79 | // NewProxy starts and run a proxy instance. 80 | func NewProxy(latency time.Duration, backend HTTPTestServer) *Proxy { 81 | proxy := NewUnstartedProxy(latency, backend) 82 | proxy.Start() 83 | return proxy 84 | } 85 | 86 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "time" 9 | "testing" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var helloHandlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | fmt.Fprint(w, "Hello client!") 15 | }) 16 | 17 | func TestStartingAndClosingProxy(t *testing.T) { 18 | delay := time.Duration(0) 19 | backend := httptest.NewServer(helloHandlerFunc) 20 | 21 | proxy := NewUnstartedProxy(delay, backend) 22 | proxy.Start() 23 | 24 | resp, _ := http.Get(proxy.URL) 25 | assert.NotNil(t, resp, "Response should not be empty") 26 | 27 | proxy.Close() 28 | _, err := http.Get(proxy.URL) 29 | assert.NotNil(t, err, "Error should not be empty") 30 | } 31 | 32 | func TestProxyConnection(t *testing.T) { 33 | assert := assert.New(t) 34 | 35 | backend := httptest.NewServer(helloHandlerFunc) 36 | latency := time.Duration(0) 37 | 38 | proxy := NewProxy(latency, backend) 39 | defer proxy.Close() 40 | 41 | resp, err := http.Get(proxy.URL) 42 | assert.Nil(err, "Error should be empty") 43 | 44 | body, err := ioutil.ReadAll(resp.Body) 45 | assert.Nil(err, "Error should be empty") 46 | 47 | assert.Equal("Hello client!", string(body), "Response text should be \"Hello client!\"") 48 | } 49 | 50 | func TestUnstartedProxyConnection(t *testing.T) { 51 | assert := assert.New(t) 52 | 53 | backend := httptest.NewServer(helloHandlerFunc) 54 | latency := time.Duration(0) 55 | proxy := NewUnstartedProxy(latency, backend) 56 | proxy.Start() 57 | defer proxy.Close() 58 | 59 | resp, err := http.Get(proxy.URL) 60 | assert.Nil(err, "Error should be empty") 61 | 62 | body, _ := ioutil.ReadAll(resp.Body) 63 | assert.Equal("Hello client!", string(body), "Response text should be \"Hello client!\"") 64 | } 65 | 66 | func TestMultipleProxies(t *testing.T) { 67 | assert := assert.New(t) 68 | 69 | backend := httptest.NewServer(helloHandlerFunc) 70 | latency := time.Duration(0) * time.Second 71 | proxy3 := NewProxy(latency, backend) 72 | proxy2 := NewProxy(latency, proxy3) 73 | proxy1 := NewProxy(latency, proxy2) 74 | 75 | defer func() { 76 | proxy3.Close() 77 | proxy2.Close() 78 | proxy1.Close() 79 | }() 80 | 81 | // Send request to the front-most proxy 82 | resp, err := http.Get(proxy1.URL) 83 | assert.Nil(err, "Error should be empty") 84 | 85 | body, _ := ioutil.ReadAll(resp.Body) 86 | assert.Equal("Hello client!", string(body), "Response text should be \"Hello client!\"") 87 | } 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /switcher.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "time" 7 | "sync" 8 | "runtime" 9 | ) 10 | 11 | // A Switcher is an HTTPTestServer placed in front of another HTTPTestServer's 12 | // to simulate a round-robin load-balancer. 13 | type Switcher struct { 14 | *httptest.Server 15 | Latency time.Duration 16 | Backends []HTTPTestServer 17 | } 18 | 19 | var ( 20 | currentServerId int 21 | mutex = &sync.Mutex{} 22 | ) 23 | 24 | // Close shuts down the switcher and blocks until all outstanding requests 25 | // on this switcher have completed. 26 | func (sw *Switcher) Close() { 27 | sw.Server.Close() 28 | } 29 | 30 | // Start starts a switcher from NewUnstartedSwitcher. 31 | func (sw *Switcher) Start() { 32 | sw.Server.Start() 33 | } 34 | 35 | // TODO: Write test for SetPort() 36 | // SetPort optionally sets a local port number a Switcher should listen on. 37 | // It should be set on an unstarted switcher only. 38 | /* 39 | func (s *Switcher) setPort(port string) { 40 | l, err := net.Listen("tcp", "127.0.0.1:" + port) 41 | if err != nil { 42 | if l, err = net.Listen("tcp6", "[::1]:" + port); err != nil { 43 | panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) 44 | } 45 | } 46 | s.Listener = l 47 | } 48 | */ 49 | 50 | // backendGenerator keeps track of the backend servers circulation. 51 | func backendGenerator(backends []HTTPTestServer) <-chan HTTPTestServer { 52 | c := make(chan HTTPTestServer) 53 | go func() <-chan HTTPTestServer { 54 | defer close(c) 55 | mutex.Lock() 56 | c <- backends[currentServerId] 57 | mutex.Unlock() 58 | if currentServerId == len(backends) - 1 { 59 | currentServerId = 0 60 | return c 61 | } 62 | mutex.Lock() 63 | currentServerId++ 64 | mutex.Unlock() 65 | runtime.Gosched() 66 | return c 67 | }() 68 | return c 69 | } 70 | 71 | // NewUnstartedProxy Start an unstarted proxy instance. 72 | func NewUnstartedSwitcher(latency time.Duration, backends []HTTPTestServer) *Switcher { 73 | middleFunc := func(w http.ResponseWriter, r *http.Request) { 74 | <-time.After(latency) 75 | func(w http.ResponseWriter, r *http.Request) { 76 | <-time.After(latency) 77 | backend := <-backendGenerator(backends) 78 | s, ok := backend.(*httptest.Server) 79 | if ok { 80 | s.Config.Handler.ServeHTTP(w, r) 81 | } else { 82 | p := backend.(*Proxy) 83 | p.Config.Handler.ServeHTTP(w, r) 84 | } 85 | }(w, r) 86 | } 87 | 88 | middleServer := httptest.NewUnstartedServer(http.HandlerFunc(middleFunc)) 89 | sw := &Switcher{ 90 | Server: middleServer, 91 | Latency: latency, 92 | Backends: backends, 93 | } 94 | return sw 95 | } 96 | 97 | // NewProxy starts and run a proxy instance. 98 | func NewSwitcher(latency time.Duration, backends []HTTPTestServer) *Switcher { 99 | sw := NewUnstartedSwitcher(latency, backends) 100 | sw.Start() 101 | return sw 102 | } 103 | 104 | 105 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Jo Chasinga. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package relay is a standard `httptest.Server` on steroid for end-to-end HTTP testing. 7 | It implements the test server with a delay middleware to simulate latency before 8 | the target test server's handler. 9 | 10 | Relay consists of two components, a Proxy and Switcher. They are HTTP middlewares 11 | which wrap the target httptest.Server's handler, thus behaving like a proxy server. 12 | It is used with httptest.Server to simulate latent proxy servers or load balancers 13 | for use in end-to-end HTTP tests. 14 | 15 | Proxy can be placed before a HTTPTestServer (httptest.Server, 16 | Proxy, or Switcher) to simulate a proxy server or a connection with some 17 | latency. It takes a latency unit and a backend HTTPTestServer as arguments. 18 | 19 | Switcher behaves similarly to a proxy, but with each request it switches 20 | between several test servers in a round-robin fashion. Switcher takes 21 | a latency unit and a []HTTPTestServer to which it will circulate requests. 22 | 23 | Let's begin setting up a basic `httptest.Server` to send request to. 24 | 25 | var handler = func(w http.ResponseWriter, r *http.Request) { 26 | fmt.Fprint(w, "Hello world!") 27 | } 28 | 29 | func TestGet(t *testing.T) { 30 | ts := httptest.NewServer(http.HandlerFunc(handler)) 31 | resp, _ := http.Get(ts.URL) 32 | b, _ := ioutil.ReadAll(resp.Body) 33 | if string(b) != "Hello world!" { 34 | t.Error("Response is not as expected.") 35 | } 36 | } 37 | 38 | Now, let's use Proxy to simulate a slow connection through which a HTTP request 39 | can be sent to the test server. 40 | 41 | delay := time.Duration(2) * time.Second 42 | timeout = time.Duration(3) * time.Second 43 | conn := relay.NewProxy(delay, ts) 44 | client := &Client{Timeout: timeout} 45 | start := time.Now() 46 | _, _ = client.Get(conn.URL) 47 | elapsed := time.Since(start) 48 | deviation := time.Duration(50) * time.Millisecond 49 | 50 | if elapsed >= timeout + deviation { 51 | t.Error("Client took too long to time out.") 52 | } 53 | 54 | Note that the latency will double because of the round trip to and from the server. 55 | 56 | Proxy can be placed in front of another proxy, and vice versa. So you can create a 57 | chain of proxies this way: 58 | 59 | delay := time.Duration(1) * time.Second 60 | ts := httptest.NewServer(http.HandlerFunc(handler)) 61 | p3 := relay.NewProxy(delay, ts) 62 | p2 := relay.NewProxy(delay, p3) 63 | p1 := relay.NewProxy(delay, p2) 64 | start := time.Now() 65 | _, _ := http.Get(p1.URL) 66 | elapsed := time.Since(start) 67 | if elapsed >= time.Duration(6) * time.Second + time.Duration(10) * time.Millisecond { 68 | t.Error("Client took longer than expected.") 69 | } 70 | 71 | Each hop to and from the target server will be delayed for one second. 72 | 73 | Switcher can be used instead of Proxy to simulate a round-robin load-balancing proxy or just to switch between several test servers' handlers for convenience. 74 | 75 | ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 76 | fmt.Fprint(w, "Hello world!") 77 | })) 78 | ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 | fmt.Fprint(w, "Hello mars!") 80 | })) 81 | ts3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 82 | fmt.Fprint(w, "Hello pluto!") 83 | })) 84 | p := relay.NewProxy(delay, ts3) 85 | sw := relay.NewSwitcher(delay, []HTTPTestServer{ts1, ts2, p}) 86 | 87 | resp1, _ := http.Get(sw.URL) // hits ts1 88 | resp2, _ := http.Get(sw.URL) // hits ts2 89 | resp3, _ := http.Get(sw.URL) // hits p, which eventually hits ts3 90 | resp4, _ := http.Get(sw.URL) // hits ts1 again, and so on 91 | */ 92 | package relay 93 | 94 | -------------------------------------------------------------------------------- /switcher_test.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import ( 4 | "io/ioutil" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "time" 9 | "testing" 10 | 11 | . "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | var ( 15 | helloMarsHandler = func(w http.ResponseWriter, r *http.Request) { 16 | fmt.Fprint(w, "Hello Mars client!") 17 | } 18 | goodDayHandler = func(w http.ResponseWriter, r *http.Request) { 19 | fmt.Fprint(w, "Good day client!") 20 | } 21 | palomaHandler = func(w http.ResponseWriter, r *http.Request) { 22 | fmt.Fprint(w, "Paloma client!") 23 | } 24 | ) 25 | 26 | func TestBasicSwitcherUtility(t *testing.T) { 27 | Convey("GIVEN an unstarted switcher", t, func() { 28 | delay := time.Duration(0) 29 | ts := httptest.NewServer(helloHandlerFunc) 30 | sw := NewUnstartedSwitcher(delay, []HTTPTestServer{ ts }) 31 | Convey("WITH a call to `Start()`", func() { 32 | sw.Start() 33 | Convey("EXPECT proxy to be running", func() { 34 | resp, _ := http.Get(sw.URL) 35 | So(resp, ShouldNotBeNil) 36 | }) 37 | }) 38 | Convey("WITH a call to `Close()`", func() { 39 | sw.Close() 40 | Convey("EXPECT proxy to be closed", func() { 41 | _, err := http.Get(sw.URL) 42 | So(err, ShouldNotBeNil) 43 | }) 44 | }) 45 | Reset(func() { 46 | ts.Close() 47 | sw.Close() 48 | }) 49 | }) 50 | } 51 | 52 | func TestBasicSwitcherConnection(t *testing.T) { 53 | Convey("GIVEN a few backend servers", t, func() { 54 | backends := []HTTPTestServer{ 55 | httptest.NewServer(http.HandlerFunc(helloMarsHandler)), 56 | httptest.NewServer(http.HandlerFunc(goodDayHandler)), 57 | httptest.NewServer(http.HandlerFunc(palomaHandler)), 58 | } 59 | 60 | Convey("GIVEN a frontend switcher", func() { 61 | latency := time.Duration(0) * time.Second 62 | sw := NewSwitcher(latency, backends) 63 | responses := []string{ 64 | "Hello Mars client!", 65 | "Good day client!", 66 | "Paloma client!", 67 | } 68 | Convey("WITH a first GET request to the switcher", func() { 69 | resp, err := http.Get(sw.URL) 70 | // TODO: Handling error here instead of asserting it. 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | // TODO: This Convey actually sends another request to the switcher, 75 | // making the test results inaccurate. 76 | // 77 | // Convey("EXPECT error to be nil", func() { 78 | // So(err, ShouldBeNil) 79 | //}) 80 | // 81 | Convey("EXPECT `Hello Mars client!` from the first backend server", func() { 82 | b, err := ioutil.ReadAll(resp.Body) 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | So(string(b), ShouldEqual, responses[0]) 87 | }) 88 | }) 89 | Convey("WITH a second GET request to the switcher", func() { 90 | resp, err := http.Get(sw.URL) 91 | // TODO: Handling error here instead of asserting it. 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | // TODO: This Convey actually sends another request to the switcher, 96 | // making the test results inaccurate. 97 | // 98 | // Convey("EXPECT error to be nil", func() { 99 | // So(err, ShouldBeNil) 100 | // }) 101 | // 102 | Convey("EXPECT `Good day client!` from the second backend server", func() { 103 | b, err := ioutil.ReadAll(resp.Body) 104 | if err != nil { 105 | t.Error(err) 106 | } 107 | So(string(b), ShouldEqual, responses[1]) 108 | }) 109 | }) 110 | Convey("WITH a third GET request to the switcher", func() { 111 | resp, err := http.Get(sw.URL) 112 | // TODO: Handling error here instead of asserting it. 113 | if err != nil { 114 | t.Error(err) 115 | } 116 | // TODO: This Convey actually sends another request to the switcher, 117 | // making the test results inaccurate. 118 | // 119 | // Convey("EXPECT error to be nil", func() { 120 | // So(err, ShouldBeNil) 121 | // }) 122 | // 123 | Convey("EXPECT `Paloma client!` from the third backend server", func() { 124 | b, err := ioutil.ReadAll(resp.Body) 125 | if err != nil { 126 | t.Error(err) 127 | } 128 | So(string(b), ShouldEqual, responses[2]) 129 | }) 130 | }) 131 | Convey("WITH a forth GET request to the switcher", func() { 132 | resp, err := http.Get(sw.URL) 133 | // TODO: Handling error here instead of asserting it. 134 | if err != nil { 135 | t.Error(err) 136 | } 137 | // TODO: This Convey actually sends another request to the switcher, 138 | // making the test results inaccurate. 139 | // 140 | // Convey("EXPECT error to be nil", func() { 141 | // So(err, ShouldBeNil) 142 | // }) 143 | // 144 | Convey("EXPECT `Hello Mars client!` from the first backend server", func() { 145 | b, err := ioutil.ReadAll(resp.Body) 146 | if err != nil { 147 | t.Error(err) 148 | } 149 | So(string(b), ShouldEqual, responses[0]) 150 | }) 151 | }) 152 | Convey("WITH a fifth GET request to the switcher", func() { 153 | resp, err := http.Get(sw.URL) 154 | // TODO: Handling error here instead of asserting it. 155 | if err != nil { 156 | t.Error(err) 157 | } 158 | // TODO: This Convey actually sends another request to the switcher, 159 | // making the test results inaccurate. 160 | // 161 | // Convey("EXPECT error to be nil", func() { 162 | // So(err, ShouldBeNil) 163 | // }) 164 | // 165 | Convey("EXPECT `Good day client!` from the second backend server", func() { 166 | b, err := ioutil.ReadAll(resp.Body) 167 | if err != nil { 168 | t.Error(err) 169 | } 170 | So(string(b), ShouldEqual, responses[1]) 171 | }) 172 | }) 173 | Reset(func() { 174 | sw.Close() 175 | }) 176 | }) 177 | Reset(func() { 178 | for _, ts := range backends { 179 | ts.Close() 180 | } 181 | }) 182 | }) 183 | } 184 | 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![relay](https://maxcdn.icons8.com/windows10/PNG/32/Industry/relay-32.png) relay 2 | ================================================================================ 3 | 4 | [![GoDoc](https://godoc.org/github.com/jochasinga/relay?status.svg)](https://godoc.org/github.com/jochasinga/relay) [![Build Status](https://drone.io/github.com/jochasinga/relay/status.png)](https://drone.io/github.com/jochasinga/relay/latest) [![Coverage Status](https://coveralls.io/repos/github/jochasinga/relay/badge.svg?branch=master)](https://coveralls.io/github/jochasinga/relay?branch=master) [![Donate](https://img.shields.io/badge/donate-$1-yellow.svg)](https://www.paypal.me/jochasinga/1.00) 5 | 6 | Powered up Go HTTP Server for comprehensive end-to-end HTTP tests. 7 | 8 | **Relay** consists of two components, `Proxy` and `Switcher`. They are both 9 | HTTP [middlewares](https://justinas.org/writing-http-middleware-in-go/) which 10 | wrap around the target server's handler to simulate latent connections, proxy 11 | servers, or load balancers. 12 | 13 | Usage 14 | ----- 15 | To use `relay` in your test, install with 16 | 17 | ```bash 18 | 19 | $ go get github.com/jochasinga/relay 20 | 21 | ``` 22 | 23 | Or better, use a [package manager](https://github.com/golang/go/wiki/PackageManagementTools) like [Godep](https://github.com/tools/godep) or [Glide](https://glide.sh/). 24 | 25 | HTTPTestServer 26 | -------------- 27 | Every instance, including the standard `httptest.Server`, implements `HTTPTestServer` 28 | interface, which means that they can be used interchangeably as a general "server". 29 | 30 | Proxy 31 | ----- 32 | A `Proxy` is used to place in front of any `HTTPTestServer` (`httptest.Server`, 33 | `Proxy`, or `Switcher`) to simulate a proxy server or a connection with some 34 | network latency. It takes a latency unit in `time.Duration` and a backend 35 | `HTTPTestServer` as arguments. 36 | 37 | Switcher 38 | -------- 39 | `Switcher` works similarly to `Proxy`, except with each request it "switches" between 40 | several backend `HTTPTestServer` in a round-robin fashion. 41 | 42 | Examples 43 | -------- 44 | Let's begin setting up a basic `httptest.Server` to send a request to. 45 | 46 | ```go 47 | 48 | var handler = func(w http.ResponseWriter, r *http.Request) { 49 | fmt.Fprint(w, "Hello world!") 50 | } 51 | 52 | func TestGet(t *testing.T) { 53 | ts := httptest.NewServer(http.HandlerFunc(handler)) 54 | resp, _ := http.Get(ts.URL) 55 | b, _ := ioutil.ReadAll(resp.Body) 56 | if string(b) != "Hello world!" { 57 | t.Error("Response is not as expected.") 58 | } 59 | } 60 | 61 | ``` 62 | 63 | Now let's use `Proxy` to simulate a slow connection through which an HTTP request 64 | can be sent to test server. 65 | 66 | ```go 67 | 68 | // Connection takes 4s to and from the test server. 69 | delay := time.Duration(2) * time.Second 70 | // Client takes 3s before timing out. 71 | timeout := time.Duration(3) * time.Second 72 | // Create a new proxy with the delay and test server backend. 73 | conn := relay.NewProxy(delay, ts) 74 | client := &Client{ Timeout: timeout } 75 | start := time.Now() 76 | _, _ := client.Get(conn.URL) 77 | elapsed := time.Since(start) 78 | deviation := time.Duration(50) * time.Millisecond 79 | 80 | if elapsed >= timeout + deviation { 81 | t.Error("Client took too long to time out.") 82 | } 83 | 84 | ``` 85 | Note that the latency will double because of the round trip to and 86 | from the server. 87 | 88 | `Proxy` can be placed in front of another proxy, and vice versa. So you 89 | can create a chain of test proxies this way: 90 | 91 | ```go 92 | 93 | delay := time.Duration(1) * time.Second 94 | ts := httptest.NewServer(http.HandlerFunc(handler)) 95 | p3 := relay.NewProxy(delay, ts) 96 | p2 := relay.NewProxy(delay, p3) 97 | p1 := relay.NewProxy(delay, p2) 98 | start := time.Now() 99 | _, _ := client.Get(p1.URL) 100 | elapsed := time.Since(start) 101 | deviation := time.Duration(50) * time.Millisecond 102 | if elapsed >= (time.Duration * 6) + deviation { 103 | t.Error("Client took longer than expected.") 104 | } 105 | 106 | ``` 107 | 108 | In the above code, Each hop to and from the target server `ts` will be delayed 109 | for one second, total to six seconds of latency. 110 | 111 | `Switcher` can be used instead of `Proxy` to simulate a round-robin load-balancing proxy 112 | or just to switch between several test servers' handlers for convenience. 113 | 114 | ```go 115 | 116 | ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 | fmt.Fprint(w, "Hello world!") 118 | })) 119 | ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 120 | fmt.Fprint(w, "Hello mars!") 121 | })) 122 | ts3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 123 | fmt.Fprint(w, "Hello pluto!") 124 | })) 125 | 126 | // a proxy sitting in front of ts3 127 | p := relay.NewProxy(delay, ts3) 128 | sw := relay.NewSwitcher(delay, []HTTPTestServer{ts1, ts2, p}) 129 | 130 | resp1, _ := http.Get(sw.URL) // hits ts1 131 | resp2, _ := http.Get(sw.URL) // hits ts2 132 | resp3, _ := http.Get(sw.URL) // hits p, which eventually hits ts3 133 | resp4, _ := http.Get(sw.URL) // hits ts1 again, and so on. 134 | 135 | ``` 136 | 137 | Also please check out this [introduction to writing test with goconvey and relay on Medium](https://medium.com/code-zen/go-http-test-with-relay-deade218fd3d#.ka0a2x19z). 138 | 139 | TODO 140 | ---- 141 | + Make `Proxy` a standalone `httptest.Server` with optional `backend=nil`. 142 | + Add other common middlewares to each proxy and switcher. 143 | + Add options to inject middleware into each proxy and switcher. 144 | + Add more "load-balancing" capabilities to `Switcher`. 145 | 146 | CONTRIBUTE 147 | ---------- 148 | Feel free to open an issue or send a pull request. 149 | Please see [goconvey](https://github.com/smartystreets/goconvey) on how to write BDD tests for relay. 150 | Contact me on twitter [@jochasinga](http://twitter.com). 151 | Fuel me with high-quality caffeine to continue working on cool code -> [![Donate](https://img.shields.io/badge/donate-$3-yellow.svg)](https://www.paypal.me/jochasinga/3.00) 152 | --------------------------------------------------------------------------------