Reputation: 771
I'm having trouble testing my go-chi routes, specifically the route with path variables. Running the server with go run main.go
works fine and requests to the route with the path variable behaves as expected.
When I run my tests for the routes, I always get the HTTP error: Unprocessable Entity
. After logging out what's happening with articleID
, it seems like the articleCtx
isn't getting access to the path variable. Not sure if this means I need to use articleCtx
in the tests, but I've tried ArticleCtx(http.HandlerFunc(GetArticleID))
and get the error:
panic: interface conversion: interface {} is nil, not *chi.Context [recovered]
panic: interface conversion: interface {} is nil, not *chi.Context
Running the server: go run main.go
Testing the server: go test .
My source:
// main.go
package main
import (
"context"
"fmt"
"net/http"
"strconv"
"github.com/go-chi/chi"
)
type ctxKey struct {
name string
}
func main() {
r := chi.NewRouter()
r.Route("/articles", func(r chi.Router) {
r.Route("/{articleID}", func(r chi.Router) {
r.Use(ArticleCtx)
r.Get("/", GetArticleID) // GET /articles/123
})
})
http.ListenAndServe(":3333", r)
}
// ArticleCtx gives the routes using it access to the requested article ID in the path
func ArticleCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
articleParam := chi.URLParam(r, "articleID")
articleID, err := strconv.Atoi(articleParam)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
ctx := context.WithValue(r.Context(), ctxKey{"articleID"}, articleID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetArticleID returns the article ID that the client requested
func GetArticleID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
articleID, ok := ctx.Value(ctxKey{"articleID"}).(int)
if !ok {
http.Error(w, http.StatusText(http.StatusUnprocessableEntity), http.StatusUnprocessableEntity)
return
}
w.Write([]byte(fmt.Sprintf("article ID:%d", articleID)))
}
// main_test.go
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestGetArticleID(t *testing.T) {
tests := []struct {
name string
rec *httptest.ResponseRecorder
req *http.Request
expectedBody string
expectedHeader string
}{
{
name: "OK_1",
rec: httptest.NewRecorder(),
req: httptest.NewRequest("GET", "/articles/1", nil),
expectedBody: `article ID:1`,
},
{
name: "OK_100",
rec: httptest.NewRecorder(),
req: httptest.NewRequest("GET", "/articles/100", nil),
expectedBody: `article ID:100`,
},
{
name: "BAD_REQUEST",
rec: httptest.NewRecorder(),
req: httptest.NewRequest("PUT", "/articles/bad", nil),
expectedBody: fmt.Sprintf("%s\n", http.StatusText(http.StatusBadRequest)),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ArticleCtx(http.HandlerFunc(GetArticleID)).ServeHTTP(test.rec, test.req)
if test.expectedBody != test.rec.Body.String() {
t.Errorf("Got: \t\t%s\n\tExpected: \t%s\n", test.rec.Body.String(), test.expectedBody)
}
})
}
}
Not sure how to continue with this. Any ideas? I was wondering if there was an answer in net/http/httptest
about using context
with tests but didn't see anything.
Also pretty new go Go (and the context
package), so any code review / best practice comments are greatly appreciated :)
Upvotes: 8
Views: 16338
Reputation: 981
I am facing the same problem, however I don't like the accepted answer that manually inject RouteContext
. When writing unit test, I want to try my best to make sure that the behavior of being tested code is as same as in real environment and of course in real life the RouteContext
is not injected by us.
Actually if you take a look at the chi source code, you will notice that the RouteContext
was injected by Mux.ServerHTTP
(https://github.com/go-chi/chi/blob/master/mux.go#L87)
So, to let chi generate the RouteContext
for you, you need to implement your test code in this way, so the unit test is testing the same code path that will be executed in real life:
func TestGetArticleID(t *testing.T) {
tests := []struct {
name string
reqPath string
expectedBody string
}{
{
name: "OK_1",
reqPath: "/articles/1",
expectedBody: `article ID:1`,
},
{
name: "OK_100",
reqPath: "/articles/100",
expectedBody: `article ID:100`,
},
{
name: "BAD_REQUEST",
reqPath: "/articles/bad",
expectedBody: fmt.Sprintf("%s\n", http.StatusText(http.StatusBadRequest)),
},
}
testServer := httptest.NewServer(BuildRouter())
defer testServer.Close()
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, testServer.URL+test.reqPath, nil)
if err != nil {
t.Fatal(err)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
respBody, err := ioutil.ReadAll(response.Body)
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
assert.Equal(t, test.expectedBody, string(respBody))
})
}
}
The function BuildRouter()
is from main.go:
type ctxKey struct {
name string
}
func main() {
http.ListenAndServe(":3333", BuildRouter())
}
// ArticleCtx gives the routes using it access to the requested article ID in the path
func ArticleCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
articleParam := chi.URLParam(r, "articleID")
articleID, err := strconv.Atoi(articleParam)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
ctx := context.WithValue(r.Context(), ctxKey{"articleID"}, articleID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetArticleID returns the article ID that the client requested
func GetArticleID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
articleID, ok := ctx.Value(ctxKey{"articleID"}).(int)
if !ok {
http.Error(w, http.StatusText(http.StatusUnprocessableEntity), http.StatusUnprocessableEntity)
return
}
w.Write([]byte(fmt.Sprintf("article ID:%d", articleID)))
}
func BuildRouter() chi.Router {
r := chi.NewRouter()
r.Route("/articles", func(r chi.Router) {
r.Route("/{articleID}", func(r chi.Router) {
r.Use(ArticleCtx)
r.Get("/", GetArticleID) // GET /articles/123
})
})
return r
}
Upvotes: 1
Reputation: 513
Probably late to the party now :) But maybe somebody else would find it useful.
I had the same issue and wanted to keep my tests structured in a slice, so I ended up creating a "helper" function in order to add required chi context to the request:
func AddChiURLParams(r *http.Request, params map[string]string) *http.Request {
ctx := chi.NewRouteContext()
for k, v := range params {
ctx.URLParams.Add(k, v)
}
return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx))
}
Again, as @Ullauri mentioned, all credit for the solution goes to soedar.
But with this function you will be able to nicely use it in the slice like so:
{
name: "OK_100",
rec: httptest.NewRecorder(),
req: AddChiURLParams(httptest.NewRequest("GET", "/articles/100", nil), map[string]string{
"id": "100",
}),
expectedBody: `article ID:100`,
},
Hope this helps! :)
Upvotes: 2
Reputation: 1960
Had a similar issue, although I was unit testing a handler directly. Basically, it seems like the url parameters are not auto added to the request context when using httptest.NewRequest
forcing you to manually add them.
Something like the following worked for me.
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/{key}", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("key", "value")
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
handler := func(w http.ResponseWriter, r *http.Request) {
value := chi.URLParam(r, "key")
}
handler(w, r)
All credit to soedar here =)
Upvotes: 18
Reputation: 1
In main
you're instructing the usage of ArticleCtx
after defining the path /articles
, but in your test you're just using ArticleCtx
directly.
Your test requests should not contain /articles
, for example:
httptest.NewRequest("GET", "/1", nil)
Upvotes: 0
Reputation: 655
I had the same problem with named path variables. I was able to resolve it setting up router for my tests. A good sample is shown in go-chi tests.
Go Chi Sample test with URL params
Upvotes: 3