Тестирование Chi-маршрутов с переменными пути

У меня проблемы с тестированием моих маршрутов го-чи, в частности маршрута с переменными пути. Запуск сервера с go run main.go работает нормально и запросы к маршруту с переменной пути ведут себя как положено.

Когда я запускаю свои тесты для маршрутов, я всегда получаю ошибку HTTP: Unprocessable Entity, После выхода из системы, что происходит с articleIDкажется, что articleCtx не получает доступ к переменной пути. Не уверен, что это означает, что мне нужно использовать articleCtx в тестах, но я пробовал ArticleCtx(http.HandlerFunc(GetArticleID)) и получите ошибку:

panic: interface conversion: interface {} is nil, not *chi.Context [recovered] panic: interface conversion: interface {} is nil, not *chi.Context

Запуск сервера: go run main.go

Тестирование сервера: go test .

Мой источник:

// 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)
            }
        })
    }
}

Не уверен, как продолжить с этим. Есть идеи? Мне было интересно, если был ответ в net/http/httptest об использовании context с тестами, но ничего не видел.

Также довольно новый Go Go (и context пакет), поэтому любые комментарии к коду / лучшие практики приветствуются:)

5 ответов

Была аналогичная проблема, хотя я напрямую тестировал обработчик. В принципе, похоже, что параметры URL-адреса не добавляются автоматически в контекст запроса при использовании httptest.NewRequestзаставляя вас добавлять их вручную.

Что-то вроде следующего работало для меня.

      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)

Вся заслуга Соедара здесь =)

У меня была такая же проблема с именованными переменными пути. Я смог решить эту проблему, настроив маршрутизатор для моих тестов. Хороший образец показан в тестах го-чи.

Пример теста Go Chi с параметрами URL

Наверное, уже опоздал на вечеринку :) Но, возможно, кому-то это будет полезно.

У меня была та же проблема, и я хотел, чтобы мои тесты были структурированы по срезам, поэтому в итоге я создал «вспомогательную» функцию, чтобы добавить к запросу необходимый контекст ци:

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

Опять же, как упомянул @Ullauri, вся заслуга в решении принадлежит soedar.

Но с помощью этой функции вы сможете прекрасно использовать ее в срезе следующим образом:

      {
    name: "OK_100",
    rec:  httptest.NewRecorder(),
    req: AddChiURLParams(httptest.NewRequest("GET", "/articles/100", nil), map[string]string{
        "id": "100",
    }),
    expectedBody: `article ID:100`,
},

Надеюсь это поможет! :)

В main вы инструктируете использование после определения пути, но в своем тесте вы просто используете ArticleCtx напрямую.

Ваши тестовые запросы не должны содержать /articles, Например:

httptest.NewRequest("GET", "/1", nil)

Я столкнулся с той же проблемой, однако мне не нравится принятый ответ, который вручную вводит файлы . При написании модульного теста я хочу сделать все возможное, чтобы убедиться, что поведение тестируемого кода такое же, как и в реальной среде, и, конечно же, в реальной жизни оно не внедряется нами. На самом деле, если вы посмотрите на исходный код chi, вы заметите, что он был введенMux.ServerHTTP(https://github.com/go-chi/chi/blob/master/mux.go#L87) Итак, чтобы позволить ци генерироватьRouteContextвам необходимо реализовать свой тестовый код таким образом, чтобы модульный тест тестировал тот же путь кода, который будет выполняться в реальной жизни:

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


ФункцияBuildRouter()взято с 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
}
Другие вопросы по тегам