Skip to content

Commit 74dcaf7

Browse files
committed
feat(testrunner): add testrunner to the project
Fixes: #2
1 parent 3ab0589 commit 74dcaf7

File tree

2 files changed

+721
-0
lines changed

2 files changed

+721
-0
lines changed

testrunner/testrunner.go

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
// Copyright 2023-2024 Flavio Garcia
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package testrunner
16+
17+
import (
18+
"bytes"
19+
"encoding/json"
20+
"errors"
21+
"fmt"
22+
"io"
23+
"net/http"
24+
"net/http/httptest"
25+
"net/url"
26+
"strings"
27+
"testing"
28+
)
29+
30+
// HttpTestRunner is a runner that facilitates testing of HTTP requests.
31+
//
32+
// It allows for configuring various aspects of the request, such as clearing
33+
// the body, header, header function, and values.
34+
type HttpTestRunner struct {
35+
// clearBody indicates whether the body should be cleared after a test run.
36+
clearBody bool
37+
38+
// clearHeader indicates whether the header should be cleared after a test
39+
// run.
40+
clearHeader bool
41+
42+
// clearHandlerFunc indicates whether the handler function should be
43+
// cleared after a test run.
44+
clearHandlerFunc bool
45+
46+
// clearValues indicates whether the header function should be cleared
47+
// after a test run.
48+
clearValues bool
49+
50+
// body represents the body of the HTTP request.
51+
body io.Reader
52+
53+
// header represents the header of the HTTP request.
54+
header http.Header
55+
56+
// handler represents the HTTP handler to be tested.
57+
handler http.Handler
58+
59+
// handlerFunc represents the HTTP handler function to be tested.
60+
handlerFunc func(http.ResponseWriter, *http.Request)
61+
62+
// method represents the HTTP method to be tested (e.g., "GET", "POST",
63+
// etc.).
64+
method string
65+
66+
// path represents the path to be tested.
67+
path string
68+
69+
// t represents the testing instance.
70+
t *testing.T
71+
72+
// values represents the URL values to be tested.
73+
values url.Values
74+
}
75+
76+
// NewHttpTestRunner creates a new HttpTestRunner, equipped with empty headers
77+
// and default HTTP method as GET.
78+
//
79+
// The root path is also set to its default value '/'.
80+
//
81+
// It's designed to streamline HTTP testing with ease and efficiency.
82+
func NewHttpTestRunner(t *testing.T) *HttpTestRunner {
83+
r := &HttpTestRunner{}
84+
r.header = http.Header{}
85+
r.method = http.MethodGet
86+
r.path = "/"
87+
r.t = t
88+
r.values = url.Values{}
89+
return r
90+
}
91+
92+
// Clear resets the HttpTestRunner by setting the body and handler func to nil
93+
// while clearing the header.
94+
//
95+
// It also resets the clearBody, clearHandlerFunc, and clearHeader flags to
96+
// false, ensuring a clean slate for future tests.
97+
func (r *HttpTestRunner) Clear() *HttpTestRunner {
98+
r.body = nil
99+
r.clearBody = false
100+
r.handlerFunc = nil
101+
r.clearHandlerFunc = false
102+
r.header = http.Header{}
103+
r.clearHeader = false
104+
r.values = url.Values{}
105+
r.clearValues = false
106+
return r
107+
}
108+
109+
// ClearBodyAfter ensures that body will be cleared after the
110+
// HttpTestRunner.Run execution.
111+
func (r *HttpTestRunner) ClearBodyAfter() *HttpTestRunner {
112+
r.clearBody = true
113+
return r
114+
}
115+
116+
// ClearHandlerFuncAfter ensures that the handler function to be tested will be
117+
// cleared after the HttpTestRunner.Run execution.
118+
func (r *HttpTestRunner) ClearHandlerFuncAfter() *HttpTestRunner {
119+
r.clearHandlerFunc = true
120+
return r
121+
}
122+
123+
// ClearHeaderAfter ensures that headers will be cleared after the
124+
// HttpTestRunner.Run execution.
125+
func (r *HttpTestRunner) ClearHeaderAfter() *HttpTestRunner {
126+
r.clearHeader = true
127+
return r
128+
}
129+
130+
// WithHandlerFunc set a function to be exectued by the runner.
131+
//
132+
// If a function is defined, it will bypass the handler.
133+
//
134+
// Use ClearFuncAfter to run the function once and clear it for the next
135+
// HttpTestRunner.Run execution.
136+
func (r *HttpTestRunner) WithHandlerFunc(
137+
handlerFunc func(http.ResponseWriter, *http.Request)) *HttpTestRunner {
138+
r.handlerFunc = handlerFunc
139+
return r
140+
}
141+
142+
// WithHandler set a handler to be executed by the runner.
143+
//
144+
// If a function is defined, it will bypass this handler.
145+
//
146+
// Use ClearFuncAfter to run the function once and clear it for the next
147+
// HttpTestRunner.Run execution.
148+
func (r *HttpTestRunner) WithHandler(handler http.Handler) *HttpTestRunner {
149+
r.handler = handler
150+
return r
151+
}
152+
153+
// WithHeader add a key/value pair to be added to the header
154+
func (r *HttpTestRunner) WithHeader(key string, value string) *HttpTestRunner {
155+
r.header.Add(key, value)
156+
return r
157+
}
158+
159+
// WithPath set the path to be executed by the runner
160+
func (r *HttpTestRunner) WithPath(path string) *HttpTestRunner {
161+
r.path = path
162+
return r
163+
}
164+
165+
// WithBody set HttpTestRunner.body using an io.Reader
166+
func (r *HttpTestRunner) WithBody(body io.Reader) *HttpTestRunner {
167+
r.body = body
168+
return r
169+
}
170+
171+
// WithJsonBody set HttpTestRunner.body using an interface
172+
func (r *HttpTestRunner) WithJsonBody(typedBody any) *HttpTestRunner {
173+
marshaledTypedRequest, _ := json.Marshal(typedBody)
174+
r.WithBody(bytes.NewReader(marshaledTypedRequest))
175+
return r
176+
}
177+
178+
// WithStringBody set HttpTestRunner.body using a string
179+
func (r *HttpTestRunner) WithStringBody(stringBody string) *HttpTestRunner {
180+
r.WithBody(bytes.NewReader([]byte(stringBody)))
181+
return r
182+
}
183+
184+
// WithMethod set the method to be used by the runner
185+
func (r *HttpTestRunner) WithMethod(method string) *HttpTestRunner {
186+
r.method = strings.ToUpper(method)
187+
return r
188+
}
189+
190+
// WithValues set the url values to be used by the runner
191+
func (r *HttpTestRunner) WithValues(values url.Values) *HttpTestRunner {
192+
r.values = values
193+
return r
194+
}
195+
196+
// runMethod executes the HTTP request method specified by the HttpTestRunner
197+
// struct.
198+
//
199+
// If runnner is running with a WithHandlerFunc, it will bypass a defined
200+
// handler.
201+
func (r *HttpTestRunner) runMethod() (*http.Response, error) {
202+
handler := r.handler
203+
if r.handlerFunc != nil {
204+
handler = http.HandlerFunc(r.handlerFunc)
205+
}
206+
s := httptest.NewServer(handler)
207+
defer s.Close()
208+
path := r.path
209+
if len(r.values) > 0 {
210+
path = path + "?" + r.values.Encode()
211+
}
212+
u, err := url.Parse(s.URL + path)
213+
if err != nil {
214+
r.t.Error(err)
215+
r.t.FailNow()
216+
}
217+
var req *http.Request
218+
req, err = http.NewRequest(r.method, u.String(), r.body)
219+
req.Header = r.header
220+
if err != nil {
221+
r.t.Error(err)
222+
r.t.FailNow()
223+
}
224+
client := &http.Client{}
225+
var res *http.Response
226+
res, err = client.Do(req)
227+
if err != nil {
228+
r.t.Error(err)
229+
r.t.FailNow()
230+
}
231+
return res, err
232+
}
233+
234+
// reset resets the state of the HttpTestRunner struct, clearing body, handler
235+
// function, header, and values if their corresponding clear flags are set to
236+
// true.
237+
func (r *HttpTestRunner) reset() {
238+
if r.clearBody {
239+
r.body = nil
240+
r.clearBody = false
241+
}
242+
if r.clearHandlerFunc {
243+
r.handlerFunc = nil
244+
r.clearHandlerFunc = false
245+
}
246+
if r.clearHeader {
247+
r.header = http.Header{}
248+
r.clearHeader = false
249+
}
250+
if r.clearValues {
251+
r.values = url.Values{}
252+
r.clearValues = false
253+
}
254+
}
255+
256+
// Run executes the HTTP request method specified by the HttpTestRunner struct
257+
// and returns the response and an error if any occurred during the execution.
258+
func (r *HttpTestRunner) Run() (res *http.Response, err error) {
259+
defer r.reset()
260+
switch r.method {
261+
case http.MethodDelete, http.MethodGet, http.MethodHead, http.MethodPatch,
262+
http.MethodPost, http.MethodPut:
263+
res, err = r.runMethod()
264+
default:
265+
res, err = nil, errors.New(
266+
fmt.Sprintf("unsupported method: %s", r.method))
267+
}
268+
return res, err
269+
}
270+
271+
// resetMethod resets the HTTP method of the HttpTestRunner struct to the
272+
// specified previous method if it is different from the current method.
273+
func (r *HttpTestRunner) resetMethod(previous string) {
274+
if previous != r.method {
275+
r.method = previous
276+
}
277+
}
278+
279+
// Delete executes an HTTP DELETE request using HttpTestRunner.Run and returns
280+
// the response and an error if any occurred during the execution.
281+
//
282+
// It will reset to the previous method in case if it wasn't http.MethodDelete.
283+
func (r *HttpTestRunner) Delete() (res *http.Response, err error) {
284+
previousMethod := r.method
285+
r.method = http.MethodDelete
286+
defer r.resetMethod(previousMethod)
287+
r.method = http.MethodDelete
288+
res, err = r.Run()
289+
return res, err
290+
}
291+
292+
// Get executes an HTTP GET request using HttpTestRunner.Run and returns
293+
// the response and an error if any occurred during the execution.
294+
//
295+
// It will reset to the previous method in case if it wasn't http.MethodGet.
296+
func (r *HttpTestRunner) Get() (res *http.Response, err error) {
297+
previousMethod := r.method
298+
r.method = http.MethodGet
299+
defer r.resetMethod(previousMethod)
300+
res, err = r.Run()
301+
return res, err
302+
}
303+
304+
// Head executes an HTTP HEAD request using HttpTestRunner.Run and returns
305+
// the response and an error if any occurred during the execution.
306+
//
307+
// It will reset to the previous method in case if it wasn't http.MethodHead.
308+
func (r *HttpTestRunner) Head() (res *http.Response, err error) {
309+
previousMethod := r.method
310+
r.method = http.MethodHead
311+
defer r.resetMethod(previousMethod)
312+
res, err = r.Run()
313+
return res, err
314+
}
315+
316+
// Patch executes an HTTP PATCH request using HttpTestRunner.Run and returns
317+
// the response and an error if any occurred during the execution.
318+
//
319+
// It will reset to the previous method in case if it wasn't http.MethodPatch.
320+
func (r *HttpTestRunner) Patch() (res *http.Response, err error) {
321+
previousMethod := r.method
322+
r.method = http.MethodPatch
323+
defer r.resetMethod(previousMethod)
324+
res, err = r.Run()
325+
return res, err
326+
}
327+
328+
// Post executes an HTTP POST request using HttpTestRunner.Run and returns
329+
// the response and an error if any occurred during the execution.
330+
//
331+
// It will reset to the previous method in case if it wasn't http.MethodPost.
332+
func (r *HttpTestRunner) Post() (res *http.Response, err error) {
333+
previousMethod := r.method
334+
r.method = http.MethodPost
335+
defer r.resetMethod(previousMethod)
336+
res, err = r.Run()
337+
return res, err
338+
}
339+
340+
// Put executes an HTTP PUT request using HttpTestRunner.Run and returns
341+
// the response and an error if any occurred during the execution.
342+
//
343+
// It will reset to the previous method in case if it wasn't http.MethodPut.
344+
func (r *HttpTestRunner) Put() (res *http.Response, err error) {
345+
previousMethod := r.method
346+
r.method = http.MethodPut
347+
defer r.resetMethod(previousMethod)
348+
res, err = r.Run()
349+
return res, err
350+
}
351+
352+
// BodyAsString returns the body of a request as string
353+
func BodyAsString(t *testing.T, res *http.Response) string {
354+
body, err := io.ReadAll(res.Body)
355+
if err != nil {
356+
t.Error(err)
357+
}
358+
return string(body)
359+
}
360+
361+
// BodyAsJson unmarshal the body of a request to json
362+
func BodyAsJson(t *testing.T, res *http.Response, jsonBody any) {
363+
b, err := io.ReadAll(res.Body)
364+
if err != nil {
365+
t.Error(err)
366+
}
367+
err = json.Unmarshal(b, jsonBody)
368+
if err != nil {
369+
t.Error(err)
370+
}
371+
}

0 commit comments

Comments
 (0)