diff --git a/README.md b/README.md index 2a62aec..50d48d7 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,20 @@ func TestApi(t *testing.T) { } ``` +#### Debugging http requests and responses generated by api test and any mocks + +```go +func TestApi(t *testing.T) { + apitest.New(). + Debug(). + Handler(handler). + Get("/hello"). + Expect(t). + Status(http.StatusOK). + End() +} +``` + #### Provide basic auth in the request ```go @@ -238,8 +252,8 @@ func TestApi(t *testing.T) { func TestApi(t *testing.T) { apitest.New(). Observe(func(res *http.Response, req *http.Request) { - // do something with res and req - }). + // do something with res and req + }). Handler(handler). Get("/hello"). Expect(t). @@ -248,22 +262,6 @@ func TestApi(t *testing.T) { } ``` -one usage for this might be debug logging to the console. The provided `DumpHttp` function does this automatically - -```go -func TestApi(t *testing.T) { - apitest.New(). - Observe(apitest.DumpHttp). - Handler(handler). - Post("/hello"). - Body(`{"a": 12345}`). - Headers(map[string]string{"Content-Type": "application/json"}). - Expect(t). - Status(http.StatusCreated). - End() -} -``` - #### Intercept the request This is useful for mutating the request before it is sent to the system under test. diff --git a/apitest.go b/apitest.go index 326939b..209411b 100644 --- a/apitest.go +++ b/apitest.go @@ -11,16 +11,21 @@ import ( "testing" ) +var divider = strings.Repeat("-", 10) +var requestDebugPrefix = fmt.Sprintf("%s>", divider) +var responseDebugPrefix = fmt.Sprintf("<%s", divider) + // APITest is the top level struct holding the test spec type APITest struct { - name string - request *Request - response *Response - observer Observe - mocks []*Mock - t *testing.T - httpClient *http.Client - transport *Transport + debugEnabled bool + name string + request *Request + response *Response + observers []Observe + mocks []*Mock + t *testing.T + httpClient *http.Client + transport *Transport } // Observe will be called by with the request and response on completion @@ -42,6 +47,11 @@ func New(name ...string) *APITest { return apiTest } +func (a *APITest) Debug() *APITest { + a.debugEnabled = true + return a +} + // Mocks is a builder method for setting the mocks func (a *APITest) Mocks(mocks ...*Mock) *APITest { var m []*Mock @@ -62,9 +72,9 @@ func (a *APITest) HttpClient(cli *http.Client) *APITest { return a } -// Observe is a builder method for setting the observer -func (a *APITest) Observe(observer Observe) *APITest { - a.observer = observer +// Observe is a builder method for setting the observers +func (a *APITest) Observe(observers ...Observe) *APITest { + a.observers = observers return a } @@ -107,19 +117,6 @@ type pair struct { r string } -// DumpHttp logs the http wire representation of the request and response -var DumpHttp Observe = func(res *http.Response, req *http.Request) { - requestDump, err := httputil.DumpRequest(req, true) - if err == nil { - fmt.Println("--> http request dump\n\n" + string(requestDump)) - } - - responseDump, err := httputil.DumpResponse(res, true) - if err == nil { - fmt.Println("<-- http response dump:\n\n" + string(responseDump)) - } -} - // Intercept is a builder method for setting the request interceptor func (r *Request) Intercept(interceptor Intercept) *Request { r.interceptor = interceptor @@ -284,7 +281,11 @@ func (r *Response) End() { apiTest := r.apiTest if len(apiTest.mocks) > 0 { - apiTest.transport = NewTransport(apiTest.mocks, apiTest.httpClient) + apiTest.transport = NewTransport( + apiTest.mocks, + apiTest.httpClient, + r.apiTest.debugEnabled, + ) defer apiTest.transport.Reset() apiTest.transport.Hijack() } @@ -294,9 +295,15 @@ func (r *Response) End() { func (a *APITest) run() { res, req := a.runTest() - if a.observer != nil { - a.observer(res.Result(), req) - } + + defer func() { + if len(a.observers) > 0 { + for _, observe := range a.observers { + observe(res.Result(), req) + } + } + }() + a.assertResponse(res) a.assertHeaders(res) a.assertCookies(res) @@ -315,7 +322,23 @@ func (a *APITest) runTest() (*httptest.ResponseRecorder, *http.Request) { a.request.interceptor(req) } res := httptest.NewRecorder() + + if a.debugEnabled { + requestDump, err := httputil.DumpRequest(req, true) + if err == nil { + debug(requestDebugPrefix,"inbound http request", string(requestDump)) + } + } + a.request.handler.ServeHTTP(res, req) + + if a.debugEnabled { + responseDump, err := httputil.DumpResponse(res.Result(), true) + if err == nil { + debug(responseDebugPrefix,"final response", string(responseDump)) + } + } + return res, req } @@ -442,3 +465,7 @@ func isJSON(s string) bool { var js map[string]interface{} return json.Unmarshal([]byte(s), &js) == nil } + +func debug(prefix, header, msg string) { + fmt.Printf("\n%s %s\n%s\n", prefix, header, msg) +} \ No newline at end of file diff --git a/apitest_test.go b/apitest_test.go index 4647378..27ecc9c 100644 --- a/apitest_test.go +++ b/apitest_test.go @@ -373,7 +373,6 @@ func TestApiTest_Observe_DumpsTheHttpRequestAndResponse(t *testing.T) { }) New(). - Observe(DumpHttp). Handler(handler). Post("/hello"). Body(`{"a": 12345}`). diff --git a/mocks.go b/mocks.go index a734395..1d00150 100644 --- a/mocks.go +++ b/mocks.go @@ -6,6 +6,7 @@ import ( "errors" "io/ioutil" "net/http" + "net/http/httputil" "net/url" "reflect" "regexp" @@ -17,15 +18,17 @@ var ( ) type Transport struct { + debugEnabled bool mocks []*Mock nativeTransport http.RoundTripper httpClient *http.Client } -func NewTransport(mocks []*Mock, httpClient *http.Client) *Transport { +func NewTransport(mocks []*Mock, httpClient *http.Client, debugEnabled bool) *Transport { t := &Transport{ - mocks: mocks, - httpClient: httpClient, + mocks: mocks, + httpClient: httpClient, + debugEnabled: debugEnabled, } if httpClient != nil { t.nativeTransport = httpClient.Transport @@ -36,12 +39,35 @@ func NewTransport(mocks []*Mock, httpClient *http.Client) *Transport { } func (r *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + var responseMock *http.Response + if r.debugEnabled { + defer func() { + debugMock(responseMock, req) + }() + } if matchedResponse := matches(req, r.mocks); matchedResponse != nil { - return buildResponseFromMock(matchedResponse), nil + responseMock = buildResponseFromMock(matchedResponse) + return responseMock, nil } return nil, errors.New(ErrFailedToMatch) } +func debugMock(res *http.Response, req *http.Request) { + requestDump, err := httputil.DumpRequestOut(req, true) + if err == nil { + debug(requestDebugPrefix,"request to mock", string(requestDump)) + } + + if res != nil { + responseDump, err := httputil.DumpResponse(res, true) + if err == nil { + debug(responseDebugPrefix,"response from mock", string(responseDump)) + } + } else { + debug(responseDebugPrefix,"response from mock", "") + } +} + func (r *Transport) Hijack() { if r.httpClient != nil { r.httpClient.Transport = r diff --git a/mocks_test.go b/mocks_test.go index 3824b09..e8fa5e6 100644 --- a/mocks_test.go +++ b/mocks_test.go @@ -338,6 +338,7 @@ func TestMocks_ApiTest_WithMocks(t *testing.T) { End() New(). + Debug(). HttpClient(test.httpCli). Mocks(getUser, getPreferences). Handler(getUserHandler(NewHttpGet(test.httpCli))).