Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhance Activate and add DeactivateNonDefault functions #161

Merged
merged 1 commit into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x, tip]
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x, tip]
full-tests: [false]
include:
- go-version: 1.23.x
Expand Down
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Easy mocking of http responses from external resources.

## Install

Currently supports Go 1.13 to 1.23 and is regularly tested against tip.
Currently supports Go 1.16 to 1.23 and is regularly tested against tip.

`v1` branch has to be used instead of `master`.

Expand All @@ -23,8 +23,7 @@ populate your `go.mod` with the latest httpmock release, now
### Simple Example:
```go
func TestFetchArticles(t *testing.T) {
httpmock.Activate()
t.Cleanup(httpmock.DeactivateAndReset)
httpmock.Activate(t)

// Exact URL match
httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles",
Expand All @@ -51,8 +50,7 @@ func TestFetchArticles(t *testing.T) {
### Advanced Example:
```go
func TestFetchArticles(t *testing.T) {
httpmock.Activate()
t.Cleanup(httpmock.DeactivateAndReset)
httpmock.Activate(t)

// our database of articles
articles := make([]map[string]interface{}, 0)
Expand Down Expand Up @@ -138,7 +136,7 @@ type MySuite struct{}

func (s *MySuite) Setup(t *td.T) error {
// block all HTTP requests
httpmock.Activate()
httpmock.Activate(t)
return nil
}

Expand All @@ -148,11 +146,6 @@ func (s *MySuite) PostTest(t *td.T, testName string) error {
return nil
}

func (s *MySuite) Destroy(t *td.T) error {
httpmock.DeactivateAndReset()
return nil
}

func TestMySuite(t *testing.T) {
tdsuite.Run(t, &MySuite{})
}
Expand Down
1 change: 1 addition & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ func TestEnv(t *testing.T) {
"expected client1.Transport to not be our DefaultTransport")
require.Not(client2.Transport, httpmock.DefaultTransport,
"expected client2.Transport to not be our DefaultTransport")
httpmock.DeactivateNonDefault(client1)
httpmock.Deactivate()
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ module github.com/jarcoal/httpmock

go 1.18

require github.com/maxatome/go-testdeep v1.12.0
require github.com/maxatome/go-testdeep v1.14.0

require github.com/davecgh/go-spew v1.1.1 // indirect
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX48g9wI=
github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
177 changes: 119 additions & 58 deletions transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"sync"
"testing"

"github.com/jarcoal/httpmock/internal"
)
Expand Down Expand Up @@ -1096,10 +1097,17 @@ var oldClientsLock sync.Mutex
// To enable mocks for a test, simply activate at the beginning of a test:
//
// func TestFetchArticles(t *testing.T) {
// httpmock.Activate()
// httpmock.Activate(t)
// // all http requests using http.DefaultTransport will now be intercepted
// }
//
// t is optional, when present it allows to automatically call
// [DeactivateAndReset] at the end of the test. It is strictly
// equivalent to:
//
// httpmock.Activate()
// t.Cleanup(httpmock.DeactivateAndReset)
//
// If you want all of your tests in a package to be mocked, just call
// [Activate] from init():
//
Expand All @@ -1113,7 +1121,7 @@ var oldClientsLock sync.Mutex
// httpmock.Activate()
// os.Exit(m.Run())
// }
func Activate() {
func Activate(t ...testing.TB) {
if Disabled() {
return
}
Expand All @@ -1124,18 +1132,30 @@ func Activate() {
InitialTransport = http.DefaultTransport
}

if len(t) > 0 && t[0] != nil {
t[0].Cleanup(DeactivateAndReset)
}

http.DefaultTransport = DefaultTransport
}

// ActivateNonDefault starts the mock environment with a non-default
// [*http.Client]. This emulates the [Activate] function, but allows for
// custom clients that do not use [http.DefaultTransport].
// [*http.Client]. This emulates the [Activate] function, but allows
// for custom clients that do not use [http.DefaultTransport]
// directly. To do so, it overrides the client Transport field with
// [DefaultTransport].
//
// To enable mocks for a test using a custom client, like:
//
// To enable mocks for a test using a custom client, activate at the
// beginning of a test:
// transport := http.DefaultTransport.(*http.Transport).Clone()
// transport.TLSHandshakeTimeout = 60 * time.Second
// client := &http.Client{Transport: transport}
//
// activate at the beginning of the test:
//
// client := &http.Client{Transport: &http.Transport{TLSHandshakeTimeout: 60 * time.Second}}
// httpmock.ActivateNonDefault(client)
//
// See also [DeactivateNonDefault], [Deactivate] and [DeactivateAndReset].
func ActivateNonDefault(client *http.Client) {
if Disabled() {
return
Expand All @@ -1150,63 +1170,56 @@ func ActivateNonDefault(client *http.Client) {
client.Transport = DefaultTransport
}

// GetCallCountInfo gets the info on all the calls httpmock has caught
// since it was activated or reset. The info is returned as a map of
// the calling keys with the number of calls made to them as their
// value. The key is the method, a space, and the URL all concatenated
// together.
// DeactivateNonDefault shuts down the mock environment for
// client. Any HTTP calls made after this will use the transport used
// before [ActivateNonDefault] call.
//
// As a special case, regexp responders generate 2 entries for each
// call. One for the call caught and the other for the rule that
// matched. For example:
//
// RegisterResponder("GET", `=~z\.com\z`, NewStringResponder(200, "body"))
// http.Get("http://z.com")
//
// will generate the following result:
//
// map[string]int{
// `GET http://z.com`: 1,
// `GET =~z\.com\z`: 1,
// }
func GetCallCountInfo() map[string]int {
return DefaultTransport.GetCallCountInfo()
}
// See also [Deactivate] and [DeactivateAndReset].
func DeactivateNonDefault(client *http.Client) {
if Disabled() {
return
}

// GetTotalCallCount gets the total number of calls httpmock has taken
// since it was activated or reset.
func GetTotalCallCount() int {
return DefaultTransport.GetTotalCallCount()
oldClientsLock.Lock()
defer oldClientsLock.Unlock()
if tr, ok := oldClients[client]; ok {
delete(oldClients, client)
client.Transport = tr
}
}

// Deactivate shuts down the mock environment. Any HTTP calls made
// after this will use a live transport.
//
// Usually you'll call it in a defer right after activating the mock
// environment:
// Usually you'll call it in a [testing.T.Cleanup] right after
// activating the mock environment:
//
// func TestFetchArticles(t *testing.T) {
// httpmock.Activate()
// defer httpmock.Deactivate()
// t.Cleanup(httpmock.Deactivate)
//
// // when this test ends, the mock environment will close
// }
//
// Since go 1.14 you can also use [*testing.T.Cleanup] method as in:
//
// func TestFetchArticles(t *testing.T) {
// httpmock.Activate()
// t.Cleanup(httpmock.Deactivate)
// Note that registered mocks and corresponding counters are not
// removed. The next time [Activate] will be called they will be
// active again. Use [DeactivateAndReset] to also remove registered
// mocks & counters.
//
// // when this test ends, the mock environment will close
// }
// It also restores all clients Transport field previously overridden
// by [ActivateNonDefault]. Unlike globally registered mocks, these
// clients won't be mocked anymore the next time [Activate] will be
// called.
//
// useful in test helpers to save your callers from calling defer themselves.
// See also [Reset] and [DeactivateAndReset].
func Deactivate() {
if Disabled() {
return
}
http.DefaultTransport = InitialTransport

if InitialTransport != nil {
http.DefaultTransport = InitialTransport
}

// reset the custom clients to use their original RoundTripper
oldClientsLock.Lock()
Expand All @@ -1219,24 +1232,72 @@ func Deactivate() {

// Reset removes any registered mocks and returns the mock
// environment to its initial state. It zeroes call counters too.
//
// See also [DeactivateAndReset].
func Reset() {
DefaultTransport.Reset()
}

// ZeroCallCounters zeroes call counters without touching registered responders.
func ZeroCallCounters() {
DefaultTransport.ZeroCallCounters()
}

// DeactivateAndReset is just a convenience method for calling
// [Deactivate] and then [Reset].
//
// Happy deferring!
// Often called at the end of the test like in:
//
// func TestFetchArticles(t *testing.T) {
// httpmock.Activate()
// t.Cleanup(httpmock.DeactivateAndReset)
//
// // when this test ends, the mock environment will close
// }
//
// you may prefer the simpler:
//
// func TestFetchArticles(t *testing.T) {
// httpmock.Activate(t)
//
// // when this test ends, the mock environment will close
// }
//
// See [Activate].
func DeactivateAndReset() {
Deactivate()
Reset()
}

// GetCallCountInfo gets the info on all the calls httpmock has caught
// since it was activated or reset. The info is returned as a map of
// the calling keys with the number of calls made to them as their
// value. The key is the method, a space, and the URL all concatenated
// together.
//
// As a special case, regexp responders generate 2 entries for each
// call. One for the call caught and the other for the rule that
// matched. For example:
//
// RegisterResponder("GET", `=~z\.com\z`, NewStringResponder(200, "body"))
// http.Get("http://z.com")
//
// will generate the following result:
//
// map[string]int{
// `GET http://z.com`: 1,
// `GET =~z\.com\z`: 1,
// }
func GetCallCountInfo() map[string]int {
return DefaultTransport.GetCallCountInfo()
}

// GetTotalCallCount gets the total number of calls httpmock has taken
// since it was first activated or last reset.
func GetTotalCallCount() int {
return DefaultTransport.GetTotalCallCount()
}

// ZeroCallCounters zeroes call counters without touching registered responders.
func ZeroCallCounters() {
DefaultTransport.ZeroCallCounters()
}

// RegisterMatcherResponder adds a new responder, associated with a given
// HTTP method, URL (or path) and [Matcher].
//
Expand Down Expand Up @@ -1310,8 +1371,8 @@ func DeactivateAndReset() {
//
// If method is a lower-cased version of CONNECT, DELETE, GET, HEAD,
// OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible
// mistake. This panic can be disabled by setting m.DontCheckMethod to
// true prior to this call.
// mistake. This panic can be disabled by setting
// [DefaultTransport].DontCheckMethod to true prior to this call.
//
// See also [RegisterResponder] if a matcher is not needed.
//
Expand Down Expand Up @@ -1422,8 +1483,8 @@ func RegisterResponder(method, url string, responder Responder) {
//
// If method is a lower-cased version of CONNECT, DELETE, GET, HEAD,
// OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible
// mistake. This panic can be disabled by setting m.DontCheckMethod to
// true prior to this call.
// mistake. This panic can be disabled by setting
// [DefaultTransport].DontCheckMethod to true prior to this call.
//
// See [RegisterRegexpResponder] if a matcher is not needed.
//
Expand Down Expand Up @@ -1458,7 +1519,7 @@ func RegisterRegexpMatcherResponder(method string, urlRegexp *regexp.Regexp, mat
// If method is a lower-cased version of CONNECT, DELETE, GET, HEAD,
// OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible
// mistake. This panic can be disabled by setting
// DefaultTransport.DontCheckMethod to true prior to this call.
// [DefaultTransport].DontCheckMethod to true prior to this call.
func RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder Responder) {
DefaultTransport.RegisterRegexpResponder(method, urlRegexp, responder)
}
Expand Down Expand Up @@ -1504,8 +1565,8 @@ func RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder
//
// If method is a lower-cased version of CONNECT, DELETE, GET, HEAD,
// OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible
// mistake. This panic can be disabled by setting m.DontCheckMethod to
// true prior to this call.
// mistake. This panic can be disabled by setting
// [DefaultTransport].DontCheckMethod to true prior to this call.
//
// See also [RegisterResponderWithQuery] if a matcher is not needed.
//
Expand Down Expand Up @@ -1592,7 +1653,7 @@ func RegisterMatcherResponderWithQuery(method, path string, query any, matcher M
// If method is a lower-cased version of CONNECT, DELETE, GET, HEAD,
// OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible
// mistake. This panic can be disabled by setting
// DefaultTransport.DontCheckMethod to true prior to this call.
// [DefaultTransport].DontCheckMethod to true prior to this call.
func RegisterResponderWithQuery(method, path string, query any, responder Responder) {
RegisterMatcherResponderWithQuery(method, path, query, Matcher{}, responder)
}
Expand Down
Loading
Loading