config
The APITest
configuration type exposes some methods to register test hooks, enabled debug logging and to define the handler under test.
Debug
Enabling debug logging will write the http wire representation of all request and response interactions to the console.
apitest.New().
Debug().
Handler(myHandler)
This will also log mock interactions. This can be useful for identifying the root cause behind test failures related to unmatched mocks. In this example the mocks do not match due to an incorrect URL in the mock definition. The request is compared with each registered mock and the reason is logged to the console for each mock mismatch
----------> inbound http request
GET /user HTTP/1.1
Host: application
failed to match mocks. Errors: received request did not match any mocks
Mock 1 mismatches:
• received path /user/12345 did not match mock path /preferences/12345
Mock 2 mismatches:
• received path /user/12345 did not match mock path /user/123456
----------> request to mock
GET /user/12345 HTTP/1.1
Host: localhost:8080
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
...
HTTP Handler
Define the handler that should be tested using either Handler
or HandlerFunc
, where myHandler
is a Go http.Handler
apitest.New().Handler(myHandler)
When setting a handler apitest does not make a HTTP call over the network. Instead the provided HTTP handler’s ServeHTTP
method is invoked in the same process as the test code. The user defined request and response structs are converted to http.Request
and http.Response
types via Go’s httptest
package. The goal here is to test the internal application and not the networking layer. This approach keeps the test fast and simple. If you would like to use a real http client to generate a request against a running application you should enable networking.
package jsonpath
import (
"net/http"
"testing"
"github.com/steinfletcher/apitest"
)
func TestHandler(t *testing.T) {
handler := http.NewServeMux()
handler.HandleFunc("/data", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
apitest.New().
Handler(handler).
Get("/data").
Expect(t).
Status(http.StatusOK).
End()
}
package jsonpath
import (
"net/http"
"testing"
"github.com/steinfletcher/apitest"
)
func TestHandlerFunc(t *testing.T) {
handlerFunc := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
apitest.New().
HandlerFunc(handlerFunc).
Post("/login").
Expect(t).
Status(http.StatusOK).
End()
}
Hooks
Intercept
Intercept
is similar to Observe
but is invoked pre request, allowing the implementer to mutate the request object before it is sent to the system under test. In this example we set the request params with a custom scheme
package main
import (
"net/http"
"testing"
"github.com/steinfletcher/apitest"
)
func TestIntercept(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
if r.URL.RawQuery != "a[]=xxx&a[]=yyy" {
t.Fatal("unexpected query")
}
w.WriteHeader(http.StatusOK)
}
apitest.New().
HandlerFunc(handler).
Intercept(func(req *http.Request) {
req.URL.RawQuery = "a[]=xxx&a[]=yyy"
}).
Get("/").
Expect(t).
Status(http.StatusOK).
End()
}
Observe
Observe
can be used to inspect the request, response and APITest instance when the test completes. This method is used internally within apitest to capture all interactions that cross the mock server which enables rendering of the test results.
package main
import (
"net/http"
"testing"
"github.com/steinfletcher/apitest"
)
func TestObserve(t *testing.T) {
var observeCalled bool
apitest.New().
Observe(func(res *http.Response, req *http.Request, apiTest *apitest.APITest) {
observeCalled = true
if http.StatusOK != res.StatusCode {
t.Fatal("unexpected status code")
}
}).
HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}).
Get("/hello").
Expect(t).
Status(http.StatusOK).
End()
if !observeCalled {
t.Fatal("Observe not called")
}
}
Networking
If you want to generate HTTP requests against a running application you should enable networking. Passing a http client with a configured cookie jar allows browser like session behaviour where cookies are preserved across multiple apitest requests. This approach can be useful for performing end-to-end tests.
package main
import (
"fmt"
"net/http"
"net/http/cookiejar"
"testing"
"time"
"github.com/steinfletcher/apitest"
)
// TestEnableNetworking creates a server with two endpoints, /login sets a token via a cookie and /authenticated_resource
// validates the token. A cookie jar is used to verify session persistence across multiple apitest instances
func TestEnableNetworking(t *testing.T) {
srv := &http.Server{Addr: "localhost:9876"}
finish := make(chan struct{})
tokenValue := "ABCDEF"
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "Token", Value: tokenValue})
w.WriteHeader(203)
})
http.HandleFunc("/authenticated_resource", func(w http.ResponseWriter, r *http.Request) {
token, err := r.Cookie("Token")
if err == http.ErrNoCookie {
w.WriteHeader(400)
return
}
if err != nil {
w.WriteHeader(500)
return
}
if token.Value != tokenValue {
t.Fatalf("token did not equal %s", tokenValue)
}
w.WriteHeader(204)
})
go func() {
if err := srv.ListenAndServe(); err != nil {
panic(err)
}
}()
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
cookieJar, _ := cookiejar.New(nil)
cli := &http.Client{
Timeout: time.Second * 1,
Jar: cookieJar,
}
apitest.New().
EnableNetworking(cli).
Get("http://localhost:9876/login").
Expect(t).
Status(203).
End()
apitest.New().
EnableNetworking(cli).
Get("http://localhost:9876/authenticated_resource").
Expect(t).
Status(204).
End()
finish <- struct{}{}
}()
<-finish
}