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()
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.
FormData("name", "Simon").
FormData("name", "Jo([a-z]+)n").
You can also require a form body key to be present (FormDataPresent
) or not present (FormDataNotPresent
)
FormDataPresent("name").
FormDataNotPresent("pets").
The JSONPath
extension provides a custom Matcher
to support matching on the request body. This is useful for partially matching on the body.
apitest.NewMock().
Post("/user-external").
AddMatcher(mocks.Equal("$.name", "jan")).
RespondWith().
package matchers
import (
"bytes"
jsonpath "github.com/steinfletcher/apitest-jsonpath/mocks"
"io/ioutil"
"net/http"
"testing"
"github.com/steinfletcher/apitest"
)
func TestMocks(t *testing.T) {
createUserMock := apitest.NewMock().
Post("/user-external").
AddMatcher(jsonpath.Equal("$.name", "jan")).
RespondWith().
Status(http.StatusCreated).
End()
apitest.New().
Mocks(createUserMock).
Handler(myHandler()).
Post("/user").
JSON(map[string]string{"name": "jan"}).
Expect(t).
Status(http.StatusCreated).
End()
}
func myHandler() *http.ServeMux {
handler := http.NewServeMux()
handler.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
_, err = http.DefaultClient.Post("http://localhost:8080/user-external", "application/json", bytes.NewReader(reqBody))
if err != nil {
panic(err)
}
w.WriteHeader(http.StatusCreated)
})
return handler
}
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()