diff --git a/README.md b/README.md
index 399ae10..d294150 100644
--- a/README.md
+++ b/README.md
@@ -10,16 +10,452 @@
**httpmock** is a mock library implementing [httptest.Server](https://golang.org/pkg/net/http/httptest/#NewServer) to
support HTTP behavioral tests.
+## Table of Contents
+
+- [Prerequisites](#prerequisites)
+- [Install](#install)
+- [Usage](#usage)
+- [Match a value](#match-a-value)
+ - [Exact](#exact)
+ - [Regexp](#regexp)
+ - [JSON](#json)
+ - [Custom Matcher](#custom-matcher)
+- [Expect a request](#expect-a-request)
+ - [Request URI](#request-uri)
+ - [Request Body](#request-body)
+ - [Request Header](#request-header)
+ - [Response Code](#response-code)
+ - [Response Header](#response-header)
+ - [Response Body](#response-body)
+- [Execution Plan](#execution-plan)
+- [Examples](#examples)
+
## Prerequisites
- `Go >= 1.16`
+[[table of contents]](#table-of-contents)
+
## Install
```bash
go get github.com/nhatthm/httpmock
```
+[[table of contents]](#table-of-contents)
+
+## Usage
+
+In a nutshell, the `httpmock.Server` is wrapper around [`httptest.Server`](https://pkg.go.dev/net/http/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`](https://github.com/stretchr/testify#mock-package). 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:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/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:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/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:
+
+- [Match a value](#match-a-value)
+- [Expect a request](#expect-a-request)
+- [Execution Plan](#execution-plan)
+
+[[table of contents]](#table-of-contents)
+
+## Match a value
+
+`httpmock` is using [`nhatthm/go-matcher`](https://github.com/nhatthm/go-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 interface{})` 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]](#table-of-contents)
+
+### Exact
+
+`matcher.Exact` matches a value by
+using [`testify/assert.ObjectsAreEqual()`](https://github.com/stretchr/testify/assert).
+
+| 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]](#table-of-contents)
+
+### Regexp
+
+`matcher.Regex` and `matcher.RegexPattern` match a value by
+using [`Regexp.MatchString`](https://pkg.go.dev/regexp#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]](#table-of-contents)
+
+### JSON
+
+`matcher.JSON` matches a value by using [`swaggest/assertjson.FailNotEqual`](https://github.com/swaggest/assertjson).
+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": ""}`)``
+
+The `""` can be used against any data types, not just the `string`. For example, `{"id": ""}`
+and `{"id": 42}` is a match.
+
+[[table of contents]](#table-of-contents)
+
+### Custom Matcher
+
+You can use your own matcher as long as it implements
+the [`matcher.Matcher`](https://github.com/nhatthm/go-matcher/blob/master/matcher.go#L12-L15) interface.
+
+[[table of contents]](#table-of-contents)
+
+## Expect a request
+
+### Request URI
+
+Use the `Server.Expect(method string, requestURI interface{})`, or `Server.Expect[METHOD](requestURI interface{})` to
+start a new expectation. You can put a `string`, a `[]byte` or a [`matcher.Matcher`](#match-a-value) for
+the `requestURI`. If the `value` is a `string` or a `[]byte`, the URI is checked by using the [`matcher.Exact`](#exact).
+
+For example:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/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]](#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 interface{})`: to match a single header.
+- `Request.WithHeaders(header map[string]interface{})`: to match multiple headers.
+
+The `value` could be `string`, `[]byte`, or a [`matcher.Matcher`](#match-a-value). If the `value` is a `string` or
+a `[]byte`, the header is checked by using the [`matcher.Exact`](#exact).
+
+For example:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/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]](#table-of-contents)
+
+### Request Body
+
+There are several ways to match a request body:
+
+- `WithBody(body interface{})`: The expected body can be a `string`, a `[]byte` or a [`matcher.Matcher`](#match-a-value)
+ . If it is a `string` or a `[]byte`, the request body is checked by [`matched.Exact`](#exact).
+- `WithBodyf(format string, args ...interface{})`: Old school `fmt.Sprintf()` call, the request body is checked
+ by [`matched.Exact`](#exact) with the result from `fmt.Sprintf()`.
+- `WithBodyJSON(body interface{})`: The expected body will be marshaled using `json.Marshal()` and the request body is
+ checked by [`matched.JSON`](#json).
+
+For example:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/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
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/httpmock"
+)
+
+func TestSimple(t *testing.T) {
+ srv := httpmock.New(func(s *httpmock.Server) {
+ s.ExpectPost("/users").
+ WithBodyJSON(map[string]interface{}{"id": 42})
+ })(t)
+
+ // Your request and assertions.
+}
+```
+
+[[table of contents]](#table-of-contents)
+
+### Response Code
+
+By default, the response code is `200`. You can change it by using `ReturnCode(code int)`
+
+For example:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/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]](#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:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/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]](#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 ...interface{})` | Same as `Return()`, but with support for formatting using `fmt.Sprintf()` | `Returnf("hello %s", "world")` |
+| `ReturnJSON(v interface{})` | 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](https://github.com/nhatthm/httpmock/blob/master/example_test.go#L44) |
+
+For example:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/nhatthm/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]](#table-of-contents)
+
+## Execution Plan
+
+The mocked HTTP server is created with the `github.com/nhatthm/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.
+
+```go
+package planner
+
+import (
+ "net/http"
+
+ "github.com/nhatthm/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`](https://github.com/nhatthm/httpmock/blob/master/example_test.go#L94))
+
+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]](#table-of-contents)
+
## Examples
```go
@@ -116,10 +552,16 @@ func TestExpectationsWereNotMet(t *testing.T) {
}
```
+[See more examples](https://github.com/nhatthm/httpmock/blob/master/example_test.go)
+
+[[table of contents]](#table-of-contents)
+
## Donation
If this project help you reduce time to develop, you can give me a cup of coffee :)
+[[table of contents]](#table-of-contents)
+
### Paypal donation
[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate/?hosted_button_id=PJZSGJN57TDJY)
@@ -127,3 +569,5 @@ If this project help you reduce time to develop, you can give me a cup of coffee
or scan this
+
+[[table of contents]](#table-of-contents)
diff --git a/example_test.go b/example_test.go
new file mode 100644
index 0000000..3c5526d
--- /dev/null
+++ b/example_test.go
@@ -0,0 +1,129 @@
+package httpmock_test
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/stretchr/testify/mock"
+
+ "github.com/nhatthm/httpmock"
+ "github.com/nhatthm/httpmock/matcher"
+ plannerMock "github.com/nhatthm/httpmock/mock/planner"
+ "github.com/nhatthm/httpmock/must"
+)
+
+func ExampleMockServer_simple() {
+ srv := httpmock.MockServer(func(s *httpmock.Server) {
+ s.ExpectGet("/hi").
+ Return(`hello world`)
+ })
+
+ requestURI := srv.URL() + "/hi"
+ req, err := http.NewRequestWithContext(context.Background(), httpmock.MethodGet, requestURI, nil)
+ must.NotFail(err)
+
+ resp, err := http.DefaultClient.Do(req)
+ must.NotFail(err)
+
+ defer resp.Body.Close() // nolint: errcheck
+
+ output, err := io.ReadAll(resp.Body)
+ must.NotFail(err)
+
+ fmt.Println(resp.Status)
+ fmt.Println(string(output))
+
+ // Output:
+ // 200 OK
+ // hello world
+}
+
+func ExampleMockServer_customHandle() {
+ srv := httpmock.MockServer(func(s *httpmock.Server) {
+ s.ExpectGet(matcher.RegexPattern(`^/uri.*`)).
+ WithHeader("Authorization", "Bearer token").
+ Run(func(r *http.Request) ([]byte, error) {
+ return []byte(r.RequestURI), nil
+ })
+ })
+
+ requestURI := srv.URL() + "/uri?a=1&b=2"
+ req, err := http.NewRequestWithContext(context.Background(), httpmock.MethodGet, requestURI, nil)
+ must.NotFail(err)
+
+ req.Header.Set("Authorization", "Bearer token")
+
+ resp, err := http.DefaultClient.Do(req)
+ must.NotFail(err)
+
+ defer resp.Body.Close() // nolint: errcheck
+
+ output, err := io.ReadAll(resp.Body)
+ must.NotFail(err)
+
+ fmt.Println(resp.Status)
+ fmt.Println(string(output))
+
+ // Output:
+ // 200 OK
+ // /uri?a=1&b=2
+}
+
+func ExampleMockServer_expectationsWereNotMet() {
+ srv := httpmock.MockServer(func(s *httpmock.Server) {
+ s.ExpectGet("/hi").
+ Return(`hello world`)
+
+ s.ExpectGet("/pay").Twice().
+ Return(`paid`)
+ })
+
+ err := srv.ExpectationsWereMet()
+
+ fmt.Println(err.Error())
+
+ // Output:
+ // there are remaining expectations that were not met:
+ // - GET /hi
+ // - GET /pay (called: 0 time(s), remaining: 2 time(s))
+}
+
+func ExampleMockServer_alwaysFailPlanner() {
+ srv := httpmock.MockServer(func(s *httpmock.Server) {
+ p := &plannerMock.Planner{}
+
+ p.On("IsEmpty").Return(false)
+ p.On("Expect", mock.Anything)
+ p.On("Plan", mock.Anything).
+ Return(nil, errors.New("always fail"))
+
+ s.WithPlanner(p)
+
+ s.ExpectGet("/hi").
+ Run(func(r *http.Request) ([]byte, error) {
+ panic(`this never happens`)
+ })
+ })
+
+ requestURI := srv.URL() + "/hi"
+ req, err := http.NewRequestWithContext(context.Background(), httpmock.MethodGet, requestURI, nil)
+ must.NotFail(err)
+
+ resp, err := http.DefaultClient.Do(req)
+ must.NotFail(err)
+
+ defer resp.Body.Close() // nolint: errcheck
+
+ output, err := io.ReadAll(resp.Body)
+ must.NotFail(err)
+
+ fmt.Println(resp.Status)
+ fmt.Println(string(output))
+
+ // Output:
+ // 500 Internal Server Error
+ // always fail
+}
diff --git a/matcher/alias.go b/matcher/alias.go
index 0c0e330..064be42 100644
--- a/matcher/alias.go
+++ b/matcher/alias.go
@@ -17,6 +17,12 @@ var Match = matcher.Match
// JSON matches two json strings with support.
var JSON = matcher.JSON
+// Regex matches two strings by using regex.
+var Regex = matcher.Regex
+
+// RegexPattern matches two strings by using regex.
+var RegexPattern = matcher.RegexPattern
+
// Exact matches two objects by their exact values.
var Exact = matcher.Exact
diff --git a/request/request.go b/request/request.go
index 419ab7a..4c726d5 100644
--- a/request/request.go
+++ b/request/request.go
@@ -3,7 +3,6 @@ package request
import (
"encoding/json"
"fmt"
- "io/ioutil"
"net/http"
"os"
"path/filepath"
@@ -225,7 +224,7 @@ func (r *Request) ReturnFile(filePath string) *Request {
return r.Run(func(*http.Request) ([]byte, error) {
// nolint:gosec // filePath is cleaned above.
- return ioutil.ReadFile(filePath)
+ return os.ReadFile(filePath)
})
}