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.

Handler example
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()
}
HandlerFunc example
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
}