Skip to content

Commit

Permalink
Merge pull request #1 from rschmied/dev
Browse files Browse the repository at this point in the history
added documentation
  • Loading branch information
rschmied committed Oct 8, 2022
2 parents faa753b + 151fa3e commit c78c290
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 15 deletions.
111 changes: 110 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,114 @@

A simple HTTP responder

(c) 2022 Ralph Schmieder
This package allows to provide mock (API) responses to client code. The responder
has a canned set of responses which can be matched against URL matched via regex.
Each response can include a status code (default is 200), returned data as `[]byte`,
and a potential `error`. Each response is only served once, if the provided data
runs out of responses, it will panic.

For this to work, our API client needs to use an interface which satisfies the `http.Client` as well
as our mock responder.

Something like this goes into the API client code:

```go
type apiClient interface {
Do(req *http.Request) (*http.Response, error)
}

type Client struct {
host string
apiToken string
httpClient apiClient
}
```

Normal operation would then store a `http.Client` instance into the
`httpClient`. When debugging, though, this can be initialized with an instance
of `mockResponder` and this will then allow to provide mocked responses.

Here's an example of a unit test:

```go
func TestClient_token_auth(t *testing.T) {

// returns a Client as defined above, having an apiClient attribute
c := NewClient("https://bla.bla")
mrClient, ctx := mr.NewMockResponder()
c.httpClient = mrClient

tests := []struct {
name string
responses mr.MockRespList
wantErr bool
errstr string
}{
{
"goodtoken",
mr.MockRespList{
mr.MockResp{
Data: []byte(`"OK"`),
},
mr.MockResp{
Data: []byte(`{"version": "2.4.1","ready": true}`),
},
},
false,
"",
},
{
"badjson",
mr.MockRespList{
mr.MockResp{
Data: []byte(`,,,`),
},
},
true,
"invalid character ',' looking for beginning of value",
},
{
"badtoken",
mr.MockRespList{
mr.MockResp{
Data: []byte(`{
"description": "No authorization token provided.",
"code": 401
}`),
Code: 401,
},
},
true,
"invalid token but no credentials provided",
},
{
"clienterror",
mr.MockRespList{
mr.MockResp{
Data: []byte{},
Err: errors.New("ka-boom"),
},
},
true,
"ka-boom",
},
}
for _, tt := range tests {
mrClient.SetData(tt.responses)
var err error
t.Run(tt.name, func(t *testing.T) {
if err = c.versionCheck(ctx); (err != nil) != tt.wantErr {
t.Errorf("Client.versionCheck() error = %v, wantErr %v", err, tt.wantErr)
}
})
if !mrClient.Empty() {
t.Error("not all data in mock client consumed")
}
if tt.wantErr {
assert.EqualError(t, err, tt.errstr)
}
}
}
```

(c) 2022 Ralph Schmieder
22 changes: 22 additions & 0 deletions mockresponder.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
"sync"
)

// MockResp is a mock response, the URL can be a RegEx, in this case the first
// response in the list of unserved responses which matches the RegEx will be
// served. If no Regex is provided, the first unserved response is served. The
// default status code is 200, can be overwritten in Code. If Err is provided,
// then this error will be returned.
type MockResp struct {
Data []byte
Code int
Expand All @@ -19,6 +24,8 @@ type MockResp struct {
served bool
}

// MockRespList is a list of mocked responses, these are the responses that the
// MockResponder serves, either sequentially or because of a RegEx match.
type MockRespList []MockResp

type contextKey string
Expand All @@ -35,6 +42,8 @@ type MockResponder struct {
mu sync.Mutex
}

// defaultDoFunc is the default implementation to return mocked responses
// as defined in the response list of the mock responder.
func defaultDoFunc(req *http.Request) (*http.Response, error) {
ctxValue := req.Context().Value(contextMockClient)
if ctxValue == nil {
Expand Down Expand Up @@ -100,37 +109,48 @@ func defaultDoFunc(req *http.Request) (*http.Response, error) {
return resp, nil
}

// Do satisfies the http.Client.Do() interface
func (m *MockResponder) Do(req *http.Request) (*http.Response, error) {
// one request at a time!
m.mu.Lock()
defer m.mu.Unlock()
return m.doFunc(req)
}

// SetDoFunc sets a new Do func which again must satisfy the http.Client.Do()
// interface. If not set, the defaultDoFunc() / built-in doFunc is used.
func (m *MockResponder) SetDoFunc(df func(req *http.Request) (*http.Response, error)) {
m.doFunc = df
}

// Reset resets the data of the responder so that it can be reused within the
// same test.
func (m *MockResponder) Reset() {
for idx := range m.mockData {
m.mockData[idx].served = false
}
m.lastServed = 0
}

// SetData sets a new mocked data response list into the mock responder.
func (m *MockResponder) SetData(data MockRespList) {
m.mockData = data
m.Reset()
}

// GetData returns the currently set mocked data response list.
func (m *MockResponder) GetData() MockRespList {
return m.mockData
}

// LastData retrieves the mocked data response which was last served.
func (m *MockResponder) LastData() []byte {
return m.mockData[m.lastServed].Data
}

// Empty returns true if all data in the mocked response list has been served.
// This can be useful at the end of the test to ensure that all data has been
// consumed which typically should be the case after a test run.
func (m *MockResponder) Empty() bool {
for _, d := range m.mockData {
if !d.served {
Expand All @@ -140,6 +160,8 @@ func (m *MockResponder) Empty() bool {
return true
}

// NewMockResponder returns a new mock responder and the accompanying context.
// During a request, the mock responder can be retrieved via the context key.
func NewMockResponder() (*MockResponder, context.Context) {
mc := &MockResponder{
doFunc: defaultDoFunc,
Expand Down
23 changes: 9 additions & 14 deletions mockresponder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,36 +135,31 @@ func TestMockResponder_PanicsInDo(t *testing.T) {
}
mrClient.SetData(data)

// context has no contextMockClient context key
req, _ := http.NewRequestWithContext(context.TODO(), http.MethodGet, "", nil)
pf1 := func() {
panicFunc := func() {
mrClient.Do(req)
}
assert.Panics(t, pf1)
assert.Panics(t, panicFunc)

// this panics because of the invalid regex set above
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, "", nil)
pf2 := func() {
mrClient.Do(req)
}
assert.Panics(t, pf2)
assert.Panics(t, panicFunc)

// this has the correct context key but the value is not a MockResponder
bogusCtx := context.WithValue(context.TODO(), contextMockClient, data)
req, _ = http.NewRequestWithContext(bogusCtx, http.MethodGet, "", nil)
pf3 := func() {
mrClient.Do(req)
}
assert.Panics(t, pf3)
assert.Panics(t, panicFunc)

var (
mri interface{}
p *MockResponder = nil
)
mri = p

// this has a nil MockResponder / interface
bogusCtx = context.WithValue(context.TODO(), contextMockClient, mri)
req, _ = http.NewRequestWithContext(bogusCtx, http.MethodGet, "", nil)
pf4 := func() {
mrClient.Do(req)
}
assert.Panics(t, pf4)
assert.Panics(t, panicFunc)

}

0 comments on commit c78c290

Please sign in to comment.