mocks

Why would I use mocks?

It is very common for an application to integrate with an external API. When running tests in the development phase a short feedback loop is desirable and it is important that the tests are repeatable and reproducible. Integrating with the real external API adds unknown factors that often cause tests to break for reasons out of your control.

Mocking external calls improves the stability of the development lifecycle testing phase helping you to ship features with confidence more quickly. This does not replace integration testing. There are no hard rules and the testing strategy will vary from project to project.

How mocks work

The mocks in apitest are heavily inspired by gock. The mock package hijacks the default HTTP transport and implements a custom RoundTrip method. If the outgoing HTTP request matches against a collection of defined mocks, the result defined in the mock will be returned to the caller.

Defining Mocks

A mock is defined by calling the apitest.NewMock() factory method.

var mock = apitest.NewMock().
    Get("http://external.com/user/12345").
    RespondWith().
    Body(`{"name": "jon"}`).
    Status(http.StatusOK).
    End()

In the above example, when a HTTP client makes a GET request to http://example.com/user/12345, then {"name": "jon"} is returned in the response body with HTTP status code 200.

The mock can then be added to the apitest configuration section as follows

apitest.New().
    Mocks(mock).
    Handler(httpHandler).
    Get("/user").
    Expect(t).
    Status(http.StatusOK).
    End()
Example
package defining_mocks

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"testing"

	"github.com/steinfletcher/apitest"
)

func TestMocks(t *testing.T) {
	getUserMock := apitest.NewMock().
		Get("/user-api").
		RespondWith().
		Body(`{"name": "jon", "id": "1234"}`).
		Status(http.StatusOK).
		End()

	getPreferencesMock := apitest.NewMock().
		Get("/preferences-api").
		RespondWith().
		Body(`{"is_contactable": false}`).
		Status(http.StatusOK).
		End()

	apitest.New().
		Mocks(getUserMock, getPreferencesMock).
		Handler(myHandler()).
		Get("/user").
		Expect(t).
		Status(http.StatusOK).
		Body(`{"name": "jon", "is_contactable": false}`).
		End()
}

func myHandler() *http.ServeMux {
	handler := http.NewServeMux()
	handler.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
		var user user
		if err := httpGet("/user-api", &user); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		var contactPreferences contactPreferences
		if err := httpGet("/preferences-api", &contactPreferences); err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		response := userResponse{
			Name:          user.Name,
			IsContactable: contactPreferences.IsContactable,
		}

		bytes, _ := json.Marshal(response)
		_, err := w.Write(bytes)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		w.WriteHeader(http.StatusOK)
	})
	return handler
}

type user struct {
	Name string `json:"name"`
	ID   string `json:"id"`
}

type contactPreferences struct {
	IsContactable bool `json:"is_contactable"`
}

type userResponse struct {
	Name          string `json:"name"`
	IsContactable bool   `json:"is_contactable"`
}

func httpGet(path string, response interface{}) error {
	res, err := http.DefaultClient.Get(fmt.Sprintf("http://localhost:8080%s", path))
	if err != nil {
		return err
	}

	bytes, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return err
	}

	err = json.Unmarshal(bytes, response)
	if err != nil {
		return err
	}

	return nil
}
Matchers

You can add matchers for the request headers, cookies, url query parameters and body.

Body

Body allows you to add a matcher for the body of the request.

var getUserMock = apitest.NewMock().
    Post("http://example.com/user/12345").
    Body(`{"username": "John"}`).
    RespondWith().
    Status(http.StatusOK).
    End()

If you are working with a URL encoded form body, you can use FormData to match a key and value. Regular expressions are also allowed as values.

var getUserMock = apitest.NewMock().
    Post("http://example.com/user/12345").
    FormData("name", "Simon").
    FormData("name", "Jo([a-z]+)n").
    RespondWith().
    Status(http.StatusOK).
    End()

You can also require a form body key to be present (FormDataPresent) or not present (FormDataNotPresent)

var getUserMock = apitest.NewMock().
    Post("http://example.com/user/12345").
    FormDataPresent("name").
    FormDataNotPresent("pets").
    RespondWith().
    Status(http.StatusOK).
    End()
Cookies

Cookie allows you to add a matcher for a cookie name and value.

var getUserMock = apitest.NewMock().
    Get("http://example.com/user/12345").
    Cookie("sessionid", "1321").
    RespondWith().
    Body(`{"name": "jon"}`).
    Status(http.StatusOK).
    End()

You can also require a cookie name to be present (CookiePresent) or not present (CookieNotPresent)

var getUserMock = apitest.NewMock().
    Get("http://example.com/user/12345").
    CookiePresent("trackingid").
    CookieNotPresent("analytics").
    RespondWith().
    Body(`{"name": "jon"}`).
    Status(http.StatusOK).
    End()
Custom matchers

You can write you own custom matcher using AddMatcher. A matcher function is defined as func(*http.Request, *MockRequest) error

var getUserMock = apitest.NewMock().
    Post("http://example.com/user/12345").
    AddMatcher(func(req *http.Request, mockReq *MockRequest) error {
    	if req.Method == http.MethodPost {
    		return nil
    	}
    	return errors.New("invalid http method")
    }).
    RespondWith().
    Status(http.StatusOK).
    End()
Header

Header allows you to add a matcher for the header key and value. Regular expressions are also allowed as values

var getUserMock = apitest.NewMock().
    Get("http://example.com/user/12345").
    Header("foo", "bar").
    Header("token", "b([a-z]+)z").
    Headers(map[string]string{"name": "John"})
    RespondWith().
    Body(`{"name": "jon"}`).
    Status(http.StatusOK).
    End()

You can also require a header to be present (HeaderPresent) or not present (HeaderNotPresent).

var getUserMock = apitest.NewMock().
    Get("http://example.com/user/12345").
    HeaderPresent("authtoken").
    HeaderNotPresent("requestid").
    RespondWith().
    Body(`{"name": "jon"}`).
    Status(http.StatusOK).
    End()
Query parameters

Query allows you to add a matcher for the a url query parameter key and value. Regular expressions are also allowed as values.

var getUserMock = apitest.NewMock().
    Get("http://example.com/user/12345").
    Query("page", "1").
    Query("name", "Jo([a-z]+)n").
    QueryParams(map[string]string{"orderBy": "ASC"}).
    RespondWith().
    Body(`{"name": "jon"}`).
    Status(http.StatusOK).
    End()

You can also require a query parameter to be present (QueryPresent) or not present (QueryNotPresent)

var getUserMock = apitest.NewMock().
    Get("http://example.com/user/12345").
    QueryPresent("page").
    QueryNotPresent("name").
    RespondWith().
    Body(`{"name": "jon"}`).
    Status(http.StatusOK).
    End()
Standalone

You can use mocks outside of API tests by using the EndStandalone termination method on the mock builder. This is useful for testing http clients outside of api tests.

func TestMocks_Standalone(t *testing.T) {
    cli := http.Client{Timeout: 5}
    defer NewMock().
        Post("http://localhost:8080/path").
        Body(`{"a", 12345}`).
        RespondWith().
        Status(http.StatusCreated).
        EndStandalone()()

    resp, err := cli.Post("http://localhost:8080/path",
        "application/json",
        strings.NewReader(`{"a", 12345}`))

    assert.NoError(t, err)
    assert.Equal(t, http.StatusCreated, resp.StatusCode)
}

EndStandalone returns a function that should be invoked after the test runs to reset the http transport to the default configuration.

If you want to register multiple standalone mocks in a test, use the apitest.NewStandaloneMocks() factory method.

resetTransport := apitest.NewStandaloneMocks(
    apitest.NewMock().
        Post("http://localhost:8080/path").
        Body(`{"a": 12345}`).
        RespondWith().
        Status(http.StatusCreated).
        End(),
    apitest.NewMock().
        Get("http://localhost:8080/path").
        RespondWith().
        Body(`{"a": 12345}`).
        Status(http.StatusOK).
        End(),
).End()
defer resetTransport()