Skip to content

Commit

Permalink
refactor: change default Server behavior to recover from panics (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
shawalli authored Aug 26, 2024
1 parent d6e7474 commit 0e39e1d
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 35 deletions.
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,10 @@ import (
)

func TestSomething(t *testing.T) {
// Setup default test server and handler to log requests and return expected responses
// You may also create your own test server, handler, and mock to manage this
// Setup default test server and handler to log requests and return expected responses.
// You may also create your own test server, handler, and mock to manage this.
ts := httpmock.NewServer()
defer ts.Close()
// Set as recoverable to log panics rather than propagate out from the server
// goroutine to the parent process
ts.Recoverable()

// Configure request mocks
expectBearerToken := func(received *http.Request) (output string, differences int) {
Expand Down Expand Up @@ -232,13 +229,12 @@ Mock.On(http.MethodGet, "/some/path", nil).RespondOK([]byte(`{"id": "1234"}`)).H

### `httpmock.Server`

#### Recoverable, IsRecoverable
#### NotRecoverable, IsRecoverable

`httpmock.Server` is a glorified version of `httptest.Server` with a default handler. With both server types, the
server runs as a goroutine. One can use `Recoverable()` to indicate that an unmatched request should not cause the
server to panic outside of the server goroutine and into the main process.

If writing a custom handler, the handler should react to a panic based on the server's `IsRecoverable()` response.
server runs as a goroutine. The default behavior is to log the panic details and recover from it. However, an
implementation can set `NotRecoverable()` to indicate to the handler that an unmatched request should cause the server
to panic outside of the server goroutine and into the main process.

## Installation

Expand Down
19 changes: 9 additions & 10 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ type Server struct {
Mock *Mock

// Whether or not panics should be caught in the server goroutine or
// allowed to propagate to the parent process. If true, the panic will be
// allowed to propagate to the parent process. If false, the panic will be
// printed and a 404 will be returned to the client.
recoverable bool
ignorePanic bool
}

// makeHandler creates a standard [http.HandlerFunc] that may be used by a
Expand Down Expand Up @@ -62,21 +62,20 @@ func NewTLSServer() *Server {
return s
}

// Recoverable sets a [Server] as recoverable, so that panics are caught and
// printed to stdout, with a final 404 returned to the client.
// NotRecoverable sets a [Server] as not recoverable, so that panics are allowed
// to propagate to the main process. With the default handler, panics are caught
// and printed to stdout, with a final 404 returned to the client.
//
// 404 was chosen rather than 500 due to panics almost always occurring when a
// matching [Request] cannot be found. However, custom handlers can choose to
// implement their recovery mechanism however they would like, using the
// [Server.IsRecoverable] method to access this value.
func (s *Server) Recoverable() *Server {
s.recoverable = true
// matching [Request] cannot be found.
func (s *Server) NotRecoverable() *Server {
s.ignorePanic = true
return s
}

// IsRecoverable returns whether or not the [Server] is considered recoverable.
func (s *Server) IsRecoverable() bool {
return s.recoverable
return !s.ignorePanic
}

// On is a convenience method to invoke the [Mock.On] method.
Expand Down
39 changes: 24 additions & 15 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,21 @@ func Test_NewTLSServer(t *testing.T) {
}
}

func TestServer_handler_NoMatch(t *testing.T) {
func TestServer_NotRecoverable(t *testing.T) {
// Setup
s := NewServer().Recoverable()
s := NewServer()
defer s.Close()

// Test
s.NotRecoverable()

// Assert
assert.True(t, s.ignorePanic)
}

func TestServer_defaultHandler_NoMatch(t *testing.T) {
// Setup
s := NewServer()
defer s.Close()
s.On(http.MethodGet, "/foo/1234", nil).RespondOK([]byte(testBody))

Expand All @@ -60,9 +72,9 @@ func TestServer_handler_NoMatch(t *testing.T) {
s.Mock.AssertNotRequested(t, http.MethodDelete, fmt.Sprintf("%s/foo/1234", s.URL), nil)
}

func TestServer_handler_AssertRequested(t *testing.T) {
func TestServer_defaultHandler_AssertRequested(t *testing.T) {
// Setup
s := NewServer().Recoverable()
s := NewServer()
defer s.Close()
s.On(http.MethodGet, "/foo/1234", nil).RespondOK([]byte(testBody))

Expand All @@ -86,19 +98,19 @@ func TestServer_handler_AssertRequested(t *testing.T) {
s.Mock.AssertRequested(t, http.MethodGet, "/foo/1234", nil)
}

func TestServer_handler_AssertNotRequested(t *testing.T) {
func TestServer_defaultHandler_AssertNotRequested(t *testing.T) {
// Setup
s := NewServer().Recoverable()
s := NewServer()
defer s.Close()
s.On(http.MethodGet, "/foo/1234", nil).RespondOK([]byte(testBody))

// Test and Assertions
s.Mock.AssertNotRequested(t, http.MethodDelete, fmt.Sprintf("%s/foo/1234", s.URL), nil)
}

func TestServer_handler_AssertExpectations(t *testing.T) {
func TestServer_defaultHandler_AssertExpectations(t *testing.T) {
// Setup
s := NewServer().Recoverable()
s := NewServer()
defer s.Close()
s.On(http.MethodGet, "/foo/1234", nil).Respond(http.StatusNotFound, nil).Twice()
s.On(http.MethodPut, "/foo/1234", []byte(testBody)).RespondNoContent()
Expand Down Expand Up @@ -163,9 +175,9 @@ func TestServer_handler_AssertExpectations(t *testing.T) {
s.Mock.AssertExpectations(t)
}

func TestServer_handler_AssertNumberOfRequests(t *testing.T) {
func TestServer_defaultHandler_AssertNumberOfRequests(t *testing.T) {
// Setup
s := NewServer().Recoverable()
s := NewServer()
defer s.Close()
s.On(http.MethodGet, "/foo/1234", nil).Respond(http.StatusNotFound, nil).Twice()
s.On(http.MethodPut, "/foo/1234", []byte(testBody)).RespondNoContent()
Expand Down Expand Up @@ -246,13 +258,10 @@ func TestServer_handler_AssertNumberOfRequests(t *testing.T) {
//
// Let's keep it as a real test to ensure it actually works!
func TestSomething(t *testing.T) {
// Setup default test server and handler to log requests and return expected responses
// You may also create your own test server, handler, and mock to manage this
// Setup default test server and handler to log requests and return expected responses.
// You may also create your own test server, handler, and mock to manage this.
ts := NewServer()
defer ts.Close()
// Set as recoverable to log panics rather than propagate out from the server
// goroutine to the parent process
ts.Recoverable()

// Configure request mocks
expectBearerToken := func(received *http.Request) (output string, differences int) {
Expand Down

0 comments on commit 0e39e1d

Please sign in to comment.