Skip to content

nhatthm/httpmock

Repository files navigation

⚠️ From v0.9.0, the project will be rebranded to go.nhat.io/httpmock. v.8.x is the last version with github.com/nhatthm/httpmock.

HTTP Mock for Golang

GitHub Releases Build Status codecov Go Report Card GoDevDoc Donate

httpmock is a mock library implementing httptest.Server to support HTTP behavioral tests.

Table of Contents

Prerequisites

  • Go >= 1.18

[table of contents]

Install

go get go.nhat.io/httpmock

[table of contents]

Usage

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:

[table of contents]

Match a value

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-"))

[table of contents]

Exact

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

[table of contents]

Regexp

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.

[table of contents]

JSON

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.

[table of contents]

Custom Matcher

You can use your own matcher as long as it implements the matcher.Matcher interface.

[table of contents]

Expect a request

Request URI

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.
}

[table of contents]

Request Header

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.
}

[table of contents]

Request Body

There are several ways to match a request body:

  • WithBody(body any): The expected body can be a string, a []byte or a matcher.Matcher . If it is a string or a []byte, the request body is checked by matched.Exact.
  • WithBodyf(format string, args ...any): Old school fmt.Sprintf() call, the request body is checked by matched.Exact with the result from fmt.Sprintf().
  • WithBodyJSON(body any): The expected body will be marshaled using json.Marshal() and the request body is checked by matched.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.
}

[table of contents]

Response Code

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.
}

[table of contents]

Response Header

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.
}

[table of contents]

Response Body

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.
}

[table of contents]

Execution Plan

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.

[table of contents]

Examples

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"}
}

See more examples

[table of contents]

Donation

If this project help you reduce time to develop, you can give me a cup of coffee :)

[table of contents]

Paypal donation

paypal

       or scan this

[table of contents]