7

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 :)

John
  • 705
  • 3
  • 9
  • 18
  • 3
    The ability to get the variable is provided by the router, that's why `r` is passed to `chi.URLParam`, however in your tests you are not setting up the router at all and so the global instance of the router has no knowledge of "articleID" since you haven't registered any pattern with that key yet. – mkopriva Feb 07 '19 at 19:33

4 Answers4

15

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 =)

Cabrera
  • 1,670
  • 1
  • 16
  • 16
3

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

Gaurav Verma
  • 645
  • 1
  • 6
  • 15
0

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)

0

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! :)

Alex Chebotarsky
  • 481
  • 5
  • 19