Skip to content

Commit b2dd342

Browse files
authoredMar 6, 2025
feat(httpclient): Added test server helper (#323)
1 parent a2d1117 commit b2dd342

File tree

5 files changed

+582
-7
lines changed

5 files changed

+582
-7
lines changed
 

‎httpclient/README.md

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@
99
> Http client module based on [net/http](https://pkg.go.dev/net/http).
1010
1111
<!-- TOC -->
12-
1312
* [Installation](#installation)
1413
* [Documentation](#documentation)
15-
* [Requests](#requests)
16-
* [Transports](#transports)
17-
* [BaseTransport](#basetransport)
18-
* [LoggerTransport](#loggertransport)
19-
* [MetricsTransport](#metricstransport)
20-
14+
* [Requests](#requests)
15+
* [Transports](#transports)
16+
* [BaseTransport](#basetransport)
17+
* [LoggerTransport](#loggertransport)
18+
* [MetricsTransport](#metricstransport)
19+
* [Testing](#testing)
2120
<!-- TOC -->
2221

2322
## Installation
@@ -224,4 +223,96 @@ map[string]string{
224223

225224
Then if the request path is `/foo/1/bar?page=2`, the metric path label will be masked with `/foo/{fooId}/bar?page={pageId}`.
226225

226+
### Testing
227+
228+
This module provides a [httpclienttest.NewTestHTTPServer()](httpclienttest/server.go) helper for testing your clients against a test server, that allows you:
229+
230+
- to define test HTTP roundtrips: a couple of test aware functions to define the request and the response behavior
231+
- to configure several test HTTP roundtrips if you need to test successive calls
232+
233+
To use it:
234+
235+
```go
236+
package main_test
237+
238+
import (
239+
"errors"
240+
"net/http"
241+
"testing"
242+
243+
"github.com/ankorstore/yokai/httpclient"
244+
"github.com/ankorstore/yokai/httpclient/httpclienttest"
245+
"github.com/stretchr/testify/assert"
246+
)
247+
248+
func TestHTTPClient(t *testing.T) {
249+
t.Parallel()
250+
251+
// client
252+
client, err := httpclient.NewDefaultHttpClientFactory().Create()
253+
assert.NoError(t, err)
254+
255+
// test server preparation
256+
testServer := httpclienttest.NewTestHTTPServer(
257+
t,
258+
// configures a roundtrip for the 1st client call
259+
httpclienttest.WithTestHTTPRoundTrip(
260+
// func to configure / assert on the client request
261+
func(tb testing.TB, req *http.Request) error {
262+
tb.Helper()
263+
264+
// performs some assertions
265+
assert.Equal(tb, "/foo", req.URL.Path)
266+
267+
// returning an error here will make the test fail, if needed
268+
return nil
269+
},
270+
// func to configure / assert on the response for the client
271+
func(tb testing.TB, w http.ResponseWriter) error {
272+
tb.Helper()
273+
274+
// prepares the response for the client
275+
w.Header.Set("foo", "bar")
276+
277+
// performs some assertions
278+
assert.Equal(tb, "bar", w.Header.Get("foo"))
279+
280+
// returning an error here will make the test fail, if needed
281+
return nil
282+
},
283+
),
284+
// configures a roundtrip for the 2nd client call
285+
httpclienttest.WithTestHTTPRoundTrip(
286+
// func to configure / assert on the client request
287+
func(tb testing.TB, req *http.Request) error {
288+
tb.Helper()
289+
290+
assert.Equal(tb, "/bar", req.URL.Path)
291+
292+
return nil
293+
},
294+
// func to configure / assert on the response for the client
295+
func(tb testing.TB, w http.ResponseWriter) error {
296+
tb.Helper()
297+
298+
w.WriteHeader(http.StatusInternalServerError)
299+
300+
return nil
301+
},
302+
),
303+
)
304+
305+
// 1st client call
306+
resp, err := client.Get(testServer.URL + "/foo")
307+
assert.NoError(t, err)
308+
assert.Equal(t, http.StatusOK, resp.StatusCode)
309+
assert.Equal(t, "bar", resp.Header.Get("foo"))
310+
311+
// 2nd client call
312+
resp, err = client.Get(testServer.URL + "/bar")
313+
assert.NoError(t, err)
314+
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
315+
}
316+
```
227317

318+
You can find more complete examples in the [tests](httpclienttest/server_test.go).

‎httpclient/httpclienttest/option.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package httpclienttest
2+
3+
type TestHTTPServerOptions struct {
4+
RoundtripsStack []TestHTTPRoundTrip
5+
}
6+
7+
func DefaultTestHTTPServerOptions() TestHTTPServerOptions {
8+
return TestHTTPServerOptions{
9+
RoundtripsStack: []TestHTTPRoundTrip{},
10+
}
11+
}
12+
13+
type TestHTTPServerOptionFunc func(o *TestHTTPServerOptions)
14+
15+
func WithTestHTTPRoundTrip(reqFunc TestHTTPRequestFunc, responseFunc TestHTTPResponseFunc) TestHTTPServerOptionFunc {
16+
return func(o *TestHTTPServerOptions) {
17+
o.RoundtripsStack = append(o.RoundtripsStack, TestHTTPRoundTrip{
18+
RequestFunc: reqFunc,
19+
ResponseFunc: responseFunc,
20+
})
21+
}
22+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package httpclienttest_test
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/ankorstore/yokai/httpclient/httpclienttest"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestTestHTTPServerOptions(t *testing.T) {
12+
t.Parallel()
13+
14+
defaultOptions := httpclienttest.DefaultTestHTTPServerOptions()
15+
assert.Len(t, defaultOptions.RoundtripsStack, 0)
16+
17+
option := httpclienttest.WithTestHTTPRoundTrip(
18+
func(tb testing.TB, req *http.Request) error {
19+
tb.Helper()
20+
21+
return nil
22+
},
23+
func(tb testing.TB, w http.ResponseWriter) error {
24+
tb.Helper()
25+
26+
return nil
27+
},
28+
)
29+
30+
option(&defaultOptions)
31+
assert.Len(t, defaultOptions.RoundtripsStack, 1)
32+
}

‎httpclient/httpclienttest/server.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package httpclienttest
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"sync"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
type TestHTTPRequestFunc func(tb testing.TB, req *http.Request) error
13+
type TestHTTPResponseFunc func(tb testing.TB, w http.ResponseWriter) error
14+
15+
type TestHTTPRoundTrip struct {
16+
RequestFunc TestHTTPRequestFunc
17+
ResponseFunc TestHTTPResponseFunc
18+
}
19+
20+
func NewTestHTTPServer(tb testing.TB, options ...TestHTTPServerOptionFunc) *httptest.Server {
21+
tb.Helper()
22+
23+
var mu sync.Mutex
24+
25+
serverOptions := DefaultTestHTTPServerOptions()
26+
for _, opt := range options {
27+
opt(&serverOptions)
28+
}
29+
30+
if len(serverOptions.RoundtripsStack) == 0 {
31+
tb.Error("test HTTP server: empty roundtrips stack")
32+
33+
return nil
34+
}
35+
36+
stackPosition := 0
37+
38+
return httptest.NewServer(
39+
http.HandlerFunc(
40+
func(w http.ResponseWriter, r *http.Request) {
41+
mu.Lock()
42+
defer mu.Unlock()
43+
44+
if stackPosition >= len(serverOptions.RoundtripsStack) {
45+
tb.Error("test HTTP server: roundtrips stack exhausted")
46+
47+
return
48+
}
49+
50+
err := serverOptions.RoundtripsStack[stackPosition].RequestFunc(tb, r)
51+
assert.NoError(tb, err)
52+
53+
err = serverOptions.RoundtripsStack[stackPosition].ResponseFunc(tb, w)
54+
assert.NoError(tb, err)
55+
56+
stackPosition++
57+
},
58+
),
59+
)
60+
}

0 commit comments

Comments
 (0)
Failed to load comments.