Skip to content

Commit

Permalink
Merge pull request #469 from coury-clark/tests-with-ctx
Browse files Browse the repository at this point in the history
Extend TestAPI interface to allow for custom context.Context
  • Loading branch information
danielgtaylor authored Jun 8, 2024
2 parents f1a50d6 + 577406f commit 40bda46
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 32 deletions.
17 changes: 17 additions & 0 deletions docs/docs/tutorial/writing-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,23 @@ Now you can run your tests!
$ go test -cover
```

You may also need to send requests with a custom [`context.Context`](https://pkg.go.dev/context#Context). For example, you may need to test an authenticated route, or test using some other request-specific values.

```go
func TestGetGreeting(t *testing.T) {
_, api := humatest.New(t)

addRoutes(api)

ctx := context.Background() // define your necessary context

resp := api.GetCtx(ctx, "/greeting/world") // provide it using the 'Ctx' suffixed methods
if !strings.Contains(resp.Body.String(), "Hello, world!") {
t.Fatalf("Unexpected response: %s", resp.Body.String())
}
}
```

## Review

Congratulations! You just learned:
Expand Down
174 changes: 142 additions & 32 deletions humatest/humatest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package humatest

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
Expand Down Expand Up @@ -41,16 +42,38 @@ func NewAdapter() huma.Adapter {
type TestAPI interface {
huma.API

// Do a request against the API. Args, if provided, should be string headers
// like `Content-Type: application/json`, an `io.Reader` for the request
// body, or a slice/map/struct which will be serialized to JSON and sent
// as the request body. Anything else will panic.
// DoCtx a request against the API with a custom [context.Context] in the
// [http.Request]. Args, if provided, should be string headers like
// `Content-Type: application/json`, an `io.Reader` for the request body, or a
// slice/map/struct which will be serialized to JSON and sent as the request
// body. Anything else will panic.
DoCtx(ctx context.Context, method, path string, args ...any) *httptest.ResponseRecorder

// Do a request against the API using [context.Background] in the [http.Request].
// Args, if provided, should be string headers like `Content-Type:
// application/json`, an `io.Reader` for the request body, or a slice/map/struct
// which will be serialized to JSON and sent as the request body. Anything else
// will panic.
Do(method, path string, args ...any) *httptest.ResponseRecorder

// Get performs a GET request against the API. Args, if provided, should be
// string headers like `Content-Type: application/json`, an `io.Reader`
// for the request body, or a slice/map/struct which will be serialized to
// JSON and sent as the request body. Anything else will panic.
// GetCtx performs a GET request against the API with a custom [context.Context]
// in the [http.Request]. Args, if provided, should be string headers like
// `Content-Type: application/json`, an `io.Reader` for the request body, or a
// slice/map/struct which will be serialized to JSON and sent as the request
// body. Anything else will panic.
//
// // Make a GET request
// api.GetCtx(ctx, "/foo")
//
// // Make a GET request with a custom header.
// api.GetCtx(ctx, "/foo", "X-My-Header: my-value")
GetCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder

// Get performs a GET request against the API using [context.Background] in the
// [http.Request]. Args, if provided, should be string headers like
// `Content-Type: application/json`, an `io.Reader` for the request body, or a
// slice/map/struct which will be serialized to JSON and sent as the request
// body. Anything else will panic.
//
// // Make a GET request
// api.Get("/foo")
Expand All @@ -59,22 +82,50 @@ type TestAPI interface {
// api.Get("/foo", "X-My-Header: my-value")
Get(path string, args ...any) *httptest.ResponseRecorder

// Post performs a POST request against the API. Args, if provided, should be
// string headers like `Content-Type: application/json`, an `io.Reader`
// for the request body, or a slice/map/struct which will be serialized to
// JSON and sent as the request body. Anything else will panic.
// PostCtx performs a POST request against the API with a custom
// [context.Context] in the [http.Request]. Args, if provided, should be string
// headers like `Content-Type: application/json`, an `io.Reader` for the request
// body, or a slice/map/struct which will be serialized to JSON and sent as the
// request body. Anything else will panic.
//
// // Make a POST request
// api.PostCtx(ctx, "/foo", bytes.NewReader(`{"foo": "bar"}`))
//
// // Make a POST request with a custom header.
// api.PostCtx(ctx, "/foo", "X-My-Header: my-value", MyBody{Foo: "bar"})
PostCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder

// Post performs a POST request against the API using [context.Background] in the
// [http.Request]. Args, if provided, should be string headers like
// `Content-Type: application/json`, an `io.Reader` for the request body, or a
// slice/map/struct which will be serialized to JSON and sent as the request
// body. Anything else will panic.
//
// // Make a POST request
// api.Post("/foo", bytes.NewReader(`{"foo": "bar"}`))
// api.Post("/foo", bytes.NewReader(`{"foo": "bar"}`))
//
// // Make a POST request with a custom header.
// api.Post("/foo", "X-My-Header: my-value", MyBody{Foo: "bar"})
Post(path string, args ...any) *httptest.ResponseRecorder

// Put performs a PUT request against the API. Args, if provided, should be
// string headers like `Content-Type: application/json`, an `io.Reader`
// for the request body, or a slice/map/struct which will be serialized to
// JSON and sent as the request body. Anything else will panic.
// PutCtx performs a PUT request against the API with a custom [context.Context]
// in the [http.Request]. Args, if provided, should be string headers like
// `Content-Type: application/json`, an `io.Reader` for the request body, or a
// slice/map/struct which will be serialized to JSON and sent as the request
// body. Anything else will panic.
//
// // Make a PUT request
// api.PutCtx(ctx, "/foo", bytes.NewReader(`{"foo": "bar"}`))
//
// // Make a PUT request with a custom header.
// api.PutCtx(ctx, "/foo", "X-My-Header: my-value", MyBody{Foo: "bar"})
PutCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder

// Put performs a PUT request against the API using [context.Background] in the
// [http.Request]. Args, if provided, should be string headers like
// `Content-Type: application/json`, an `io.Reader` for the request body, or a
// slice/map/struct which will be serialized to JSON and sent as the request
// body. Anything else will panic.
//
// // Make a PUT request
// api.Put("/foo", bytes.NewReader(`{"foo": "bar"}`))
Expand All @@ -83,10 +134,24 @@ type TestAPI interface {
// api.Put("/foo", "X-My-Header: my-value", MyBody{Foo: "bar"})
Put(path string, args ...any) *httptest.ResponseRecorder

// Patch performs a PATCH request against the API. Args, if provided, should
// be string headers like `Content-Type: application/json`, an `io.Reader`
// for the request body, or a slice/map/struct which will be serialized to
// JSON and sent as the request body. Anything else will panic.
// PatchCtx performs a PATCH request against the API with a custom
// [context.Context] in the [http.Request]. Args, if provided, should be string
// headers like `Content-Type: application/json`, an `io.Reader` for the request
// body, or a slice/map/struct which will be serialized to JSON and sent as the
// request body. Anything else will panic.
//
// // Make a PATCH request
// api.PatchCtx(ctx, "/foo", bytes.NewReader(`{"foo": "bar"}`))
//
// // Make a PATCH request with a custom header.
// api.PatchCtx(ctx, "/foo", "X-My-Header: my-value", MyBody{Foo: "bar"})
PatchCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder

// Patch performs a PATCH request against the API using [context.Background] in
// the [http.Request]. Args, if provided, should be string headers like
// `Content-Type: application/json`, an `io.Reader` for the request body, or a
// slice/map/struct which will be serialized to JSON and sent as the request
// body. Anything else will panic.
//
// // Make a PATCH request
// api.Patch("/foo", bytes.NewReader(`{"foo": "bar"}`))
Expand All @@ -95,25 +160,45 @@ type TestAPI interface {
// api.Patch("/foo", "X-My-Header: my-value", MyBody{Foo: "bar"})
Patch(path string, args ...any) *httptest.ResponseRecorder

// Delete performs a DELETE request against the API. Args, if provided, should
// be string headers like `Content-Type: application/json`, an `io.Reader`
// for the request body, or a slice/map/struct which will be serialized to
// JSON and sent as the request body. Anything else will panic.
// DeleteCtx performs a DELETE request against the API with a custom
// [context.Context] in the [http.Request]. Args, if provided, should be string
// headers like `Content-Type: application/json`, an `io.Reader` for the request
// body, or a slice/map/struct which will be serialized to JSON and sent as the
// request body. Anything else will panic.
//
// // Make a DELETE request
// api.DeleteCtx(ctx, "/foo")
//
// // Make a DELETE request with a custom header.
// api.DeleteCtx(ctx, "/foo", "X-My-Header: my-value")
DeleteCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder

// Delete performs a DELETE request against the API using [context.Background] in
// the [http.Request]. Args, if provided, should be string headers like
// `Content-Type: application/json`, an `io.Reader` for the request body, or a
// slice/map/struct which will be serialized to JSON and sent as the request
// body. Anything else will panic.
//
// // Make a DELETE request
// api.Delete("/foo")
// api.Delete("/foo")
//
// // Make a DELETE request with a custom header.
// api.Delete("/foo", "X-My-Header: my-value")
Delete(path string, args ...any) *httptest.ResponseRecorder
}

var _ TestAPI = &testAPI{}

type testAPI struct {
huma.API
tb TB
}

func (a *testAPI) Do(method, path string, args ...any) *httptest.ResponseRecorder {
return a.DoCtx(context.Background(), method, path, args...)
}

func (a *testAPI) DoCtx(ctx context.Context, method, path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
var b io.Reader
isJSON := false
Expand All @@ -136,7 +221,7 @@ func (a *testAPI) Do(method, path string, args ...any) *httptest.ResponseRecorde
}
}

req, _ := http.NewRequest(method, path, b)
req, _ := http.NewRequestWithContext(ctx, method, path, b)
req.RequestURI = path
req.RemoteAddr = "127.0.0.1:12345"
if isJSON {
Expand Down Expand Up @@ -167,27 +252,52 @@ func (a *testAPI) Do(method, path string, args ...any) *httptest.ResponseRecorde

func (a *testAPI) Get(path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.Do(http.MethodGet, path, args...)
return a.GetCtx(context.Background(), path, args...)
}

func (a *testAPI) GetCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.DoCtx(ctx, http.MethodGet, path, args...)
}

func (a *testAPI) Post(path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.Do(http.MethodPost, path, args...)
return a.PostCtx(context.Background(), path, args...)
}

func (a *testAPI) PostCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.DoCtx(ctx, http.MethodPost, path, args...)
}

func (a *testAPI) Put(path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.Do(http.MethodPut, path, args...)
return a.PutCtx(context.Background(), path, args...)
}

func (a *testAPI) PutCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.DoCtx(ctx, http.MethodPut, path, args...)
}

func (a *testAPI) Patch(path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.Do(http.MethodPatch, path, args...)
return a.PatchCtx(context.Background(), path, args...)
}

func (a *testAPI) PatchCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.DoCtx(ctx, http.MethodPatch, path, args...)
}

func (a *testAPI) Delete(path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.Do(http.MethodDelete, path, args...)
return a.DeleteCtx(context.Background(), path, args...)
}

func (a *testAPI) DeleteCtx(ctx context.Context, path string, args ...any) *httptest.ResponseRecorder {
a.tb.Helper()
return a.DoCtx(ctx, http.MethodDelete, path, args...)
}

// Wrap returns a `TestAPI` wrapping the given API.
Expand Down

0 comments on commit 40bda46

Please sign in to comment.