Skip to content

Commit d6957ab

Browse files
feat: package benchttptest (#62)
feat: create package benchttptest - implement AssertEqualRunners, EqualRunners, DiffRunner - Unit test exposed functions
1 parent 5a25fcb commit d6957ab

File tree

5 files changed

+338
-0
lines changed

5 files changed

+338
-0
lines changed

benchttptest/benchttptest.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package benchttptest proovides utilities for benchttp testing.
2+
package benchttptest

benchttptest/compare.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package benchttptest
2+
3+
import (
4+
"bytes"
5+
"crypto/tls"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
"strings"
10+
"testing"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
14+
15+
"github.com/benchttp/sdk/benchttp"
16+
)
17+
18+
// RunnerCmpOptions is the cmp.Options used to compare benchttp.Runner.
19+
// By default, it ignores unexported fields and includes RequestCmpOptions.
20+
var RunnerCmpOptions = cmp.Options{
21+
cmpopts.IgnoreUnexported(benchttp.Runner{}),
22+
RequestCmpOptions,
23+
}
24+
25+
// RequestCmpOptions is the cmp.Options used to compare *http.Request.
26+
// It behaves as follows:
27+
//
28+
// - Nil and empty values are considered equal
29+
//
30+
// - Fields that depend on how the request was created are ignored
31+
// to avoid false negatives when comparing requests created in different
32+
// ways (http.NewRequest vs httptest.NewRequest vs &http.Request{})
33+
//
34+
// - Function fields are ignored
35+
//
36+
// - Body is ignored: it is compared separately
37+
var RequestCmpOptions = cmp.Options{
38+
cmp.Transformer("Request", instantiateNilRequest),
39+
cmp.Transformer("Request.Header", instantiateNilHeader),
40+
cmp.Transformer("Request.URL", stringifyURL),
41+
cmpopts.IgnoreUnexported(http.Request{}, tls.ConnectionState{}),
42+
cmpopts.IgnoreFields(http.Request{}, unreliableRequestFields...),
43+
}
44+
45+
var unreliableRequestFields = []string{
46+
// These fields are automatically set by NewRequest constructor
47+
// from packages http and httptest, as a consequence they can
48+
// trigger false positives when comparing requests that were
49+
// created differently.
50+
"Proto", "ProtoMajor", "ProtoMinor", "ContentLength",
51+
"Host", "RemoteAddr", "RequestURI", "TLS", "Cancel",
52+
53+
// Function fields cannot be reliably compared
54+
"GetBody",
55+
56+
// Body field can't be read without altering the Request, causing
57+
// cmp-go to panic. We perform a custom comparison instead.
58+
"Body",
59+
}
60+
61+
// AssertEqualRunners fails t and shows a diff if a and b are not equal,
62+
// as determined by RunnerCmpOptions.
63+
func AssertEqualRunners(t *testing.T, x, y benchttp.Runner) {
64+
t.Helper()
65+
if !EqualRunners(x, y) {
66+
t.Error(DiffRunner(x, y))
67+
}
68+
}
69+
70+
// EqualRunners returns true if x and y are equal, as determined by
71+
// RunnerCmpOptions.
72+
func EqualRunners(x, y benchttp.Runner) bool {
73+
return cmp.Equal(x, y, RunnerCmpOptions) &&
74+
compareRequestBody(x.Request, y.Request)
75+
}
76+
77+
// DiffRunner returns a string showing the diff between x and y,
78+
// as determined by RunnerCmpOptions.
79+
func DiffRunner(x, y benchttp.Runner) string {
80+
b := strings.Builder{}
81+
b.WriteString(cmp.Diff(x, y, RunnerCmpOptions))
82+
if x.Request != nil && y.Request != nil {
83+
xbody := nopreadBody(x.Request)
84+
ybody := nopreadBody(y.Request)
85+
if !bytes.Equal(xbody, ybody) {
86+
b.WriteString("Request.Body: ")
87+
b.WriteString(cmp.Diff(string(xbody), string(ybody)))
88+
}
89+
}
90+
return b.String()
91+
}
92+
93+
// helpers
94+
95+
func instantiateNilRequest(r *http.Request) *http.Request {
96+
if r == nil {
97+
return &http.Request{}
98+
}
99+
return r
100+
}
101+
102+
func instantiateNilHeader(h http.Header) http.Header {
103+
if h == nil {
104+
return http.Header{}
105+
}
106+
return h
107+
}
108+
109+
func stringifyURL(u *url.URL) string {
110+
if u == nil {
111+
return ""
112+
}
113+
return u.String()
114+
}
115+
116+
func compareRequestBody(a, b *http.Request) bool {
117+
ba, bb := nopreadBody(a), nopreadBody(b)
118+
return bytes.Equal(ba, bb)
119+
}
120+
121+
func nopreadBody(r *http.Request) []byte {
122+
if r == nil || r.Body == nil {
123+
return []byte{}
124+
}
125+
126+
bbuf := bytes.Buffer{}
127+
128+
if _, err := io.Copy(&bbuf, r.Body); err != nil {
129+
panic("benchttptest: error reading Request.Body: " + err.Error())
130+
}
131+
132+
if r.GetBody != nil {
133+
newbody, err := r.GetBody()
134+
if err != nil {
135+
panic("benchttptest: Request.GetBody error: " + err.Error())
136+
}
137+
r.Body = newbody
138+
} else {
139+
r.Body = io.NopCloser(&bbuf)
140+
}
141+
142+
return bbuf.Bytes()
143+
}

benchttptest/compare_test.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package benchttptest_test
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"net/url"
9+
"strings"
10+
"testing"
11+
12+
"github.com/benchttp/sdk/benchttp"
13+
"github.com/benchttp/sdk/benchttptest"
14+
)
15+
16+
func TestAssertEqualRunners(t *testing.T) {
17+
for _, tc := range []struct {
18+
name string
19+
pass bool
20+
a, b benchttp.Runner
21+
}{
22+
{
23+
name: "pass if runners are equal",
24+
pass: true,
25+
a: benchttp.Runner{Requests: 1},
26+
b: benchttp.Runner{Requests: 1},
27+
},
28+
{
29+
name: "fail if runners are not equal",
30+
pass: false,
31+
a: benchttp.Runner{Requests: 1},
32+
b: benchttp.Runner{Requests: 2},
33+
},
34+
} {
35+
t.Run(tc.name, func(t *testing.T) {
36+
tt := &testing.T{}
37+
38+
benchttptest.AssertEqualRunners(tt, tc.a, tc.b)
39+
if tt.Failed() == tc.pass {
40+
t.Fail()
41+
}
42+
})
43+
}
44+
}
45+
46+
func TestEqualRunners(t *testing.T) {
47+
for _, tc := range []struct {
48+
name string
49+
want bool
50+
a, b benchttp.Runner
51+
}{
52+
{
53+
name: "equal runners",
54+
want: true,
55+
a: benchttp.Runner{Requests: 1},
56+
b: benchttp.Runner{Requests: 1},
57+
},
58+
{
59+
name: "different runners",
60+
want: false,
61+
a: benchttp.Runner{Requests: 1},
62+
b: benchttp.Runner{Requests: 2},
63+
},
64+
{
65+
name: "consider zero requests equal",
66+
want: true,
67+
a: benchttp.Runner{Request: nil},
68+
b: benchttp.Runner{Request: &http.Request{}},
69+
},
70+
{
71+
name: "consider zero request headers equal",
72+
want: true,
73+
a: benchttp.Runner{Request: &http.Request{Header: nil}},
74+
b: benchttp.Runner{Request: &http.Request{Header: http.Header{}}},
75+
},
76+
{
77+
name: "consider zero request bodies equal",
78+
want: true,
79+
a: benchttp.Runner{Request: &http.Request{Body: nil}},
80+
b: benchttp.Runner{Request: &http.Request{Body: http.NoBody}},
81+
},
82+
{
83+
name: "zero request vs non zero request",
84+
want: false,
85+
a: benchttp.Runner{Request: &http.Request{Method: "GET"}},
86+
b: benchttp.Runner{Request: nil},
87+
},
88+
{
89+
name: "different request field values",
90+
want: false,
91+
a: benchttp.Runner{Request: &http.Request{Method: "GET"}},
92+
b: benchttp.Runner{Request: &http.Request{Method: "POST"}},
93+
},
94+
{
95+
name: "ignore unreliable request fields",
96+
want: true,
97+
a: benchttp.Runner{
98+
Request: httptest.NewRequest( // sets Proto, ContentLength, ...
99+
"POST",
100+
"https://example.com",
101+
nil,
102+
),
103+
},
104+
b: benchttp.Runner{
105+
Request: &http.Request{
106+
Method: "POST",
107+
URL: mustParseRequestURI("https://example.com"),
108+
},
109+
},
110+
},
111+
{
112+
name: "equal request bodies",
113+
want: true,
114+
a: benchttp.Runner{
115+
Request: &http.Request{
116+
Body: io.NopCloser(strings.NewReader("hello")),
117+
},
118+
},
119+
b: benchttp.Runner{
120+
Request: &http.Request{
121+
Body: io.NopCloser(strings.NewReader("hello")),
122+
},
123+
},
124+
},
125+
{
126+
name: "different request bodies",
127+
want: false,
128+
a: benchttp.Runner{
129+
Request: &http.Request{
130+
Body: io.NopCloser(strings.NewReader("hello")),
131+
},
132+
},
133+
b: benchttp.Runner{
134+
Request: &http.Request{
135+
Body: io.NopCloser(strings.NewReader("world")),
136+
},
137+
},
138+
},
139+
} {
140+
t.Run(tc.name, func(t *testing.T) {
141+
if benchttptest.EqualRunners(tc.a, tc.b) != tc.want {
142+
t.Error(benchttptest.DiffRunner(tc.a, tc.b))
143+
}
144+
})
145+
}
146+
147+
t.Run("restore request body", func(t *testing.T) {
148+
a := benchttp.Runner{
149+
Request: httptest.NewRequest(
150+
"POST",
151+
"https://example.com",
152+
strings.NewReader("hello"),
153+
),
154+
}
155+
b := benchttp.Runner{
156+
Request: &http.Request{
157+
Method: "POST",
158+
URL: mustParseRequestURI("https://example.com"),
159+
Body: io.NopCloser(bytes.NewReader([]byte("hello"))),
160+
},
161+
}
162+
163+
_ = benchttptest.EqualRunners(a, b)
164+
165+
ba, bb := mustRead(a.Request.Body), mustRead(b.Request.Body)
166+
want := []byte("hello")
167+
if !bytes.Equal(want, ba) || !bytes.Equal(want, bb) {
168+
t.Fail()
169+
}
170+
})
171+
}
172+
173+
// helpers
174+
175+
func mustParseRequestURI(s string) *url.URL {
176+
u, err := url.ParseRequestURI(s)
177+
if err != nil {
178+
panic(err)
179+
}
180+
return u
181+
}
182+
183+
func mustRead(r io.Reader) []byte {
184+
b, err := io.ReadAll(r)
185+
if err != nil {
186+
panic("mustRead: " + err.Error())
187+
}
188+
return b
189+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ require (
66
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
77
gopkg.in/yaml.v3 v3.0.1
88
)
9+
10+
require github.com/google/go-cmp v0.5.9

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
2+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
13
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
24
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
35
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

0 commit comments

Comments
 (0)