Тестирование модулей для функций, которые используют параметры URL-адреса gorilla/mux
Вот что я пытаюсь сделать:
main.go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
mainRouter := mux.NewRouter().StrictSlash(true)
mainRouter.HandleFunc("/test/{mystring}", GetRequest).Name("/test/{mystring}").Methods("GET")
http.Handle("/", mainRouter)
err := http.ListenAndServe(":8080", mainRouter)
if err != nil {
fmt.Println("Something is wrong : " + err.Error())
}
}
func GetRequest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
myString := vars["mystring"]
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(myString))
}
Это создает базовый HTTP-сервер, прослушивающий порт 8080
, который перекликается с параметром URL, указанным в пути. Поэтому для http://localhost:8080/test/abcd
он напишет ответ, содержащий abcd
в теле ответа.
unit test для функции GetRequest()
находится в main_test.go:
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/context"
"github.com/stretchr/testify/assert"
)
func TestGetRequest(t *testing.T) {
t.Parallel()
r, _ := http.NewRequest("GET", "/test/abcd", nil)
w := httptest.NewRecorder()
//Hack to try to fake gorilla/mux vars
vars := map[string]string{
"mystring": "abcd",
}
context.Set(r, 0, vars)
GetRequest(w, r)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, []byte("abcd"), w.Body.Bytes())
}
Результат теста:
--- FAIL: TestGetRequest (0.00s)
assertions.go:203:
Error Trace: main_test.go:27
Error: Not equal: []byte{0x61, 0x62, 0x63, 0x64} (expected)
!= []byte(nil) (actual)
Diff:
--- Expected
+++ Actual
@@ -1,4 +1,2 @@
-([]uint8) (len=4 cap=8) {
- 00000000 61 62 63 64 |abcd|
-}
+([]uint8) <nil>
FAIL
FAIL command-line-arguments 0.045s
Вопрос в том, как подделать mux.Vars(r)
для модульных тестов?
Я нашел несколько обсуждений здесь, но предлагаемое решение больше не работает. Предлагаемое решение было:
func buildRequest(method string, url string, doctype uint32, docid uint32) *http.Request {
req, _ := http.NewRequest(method, url, nil)
req.ParseForm()
var vars = map[string]string{
"doctype": strconv.FormatUint(uint64(doctype), 10),
"docid": strconv.FormatUint(uint64(docid), 10),
}
context.DefaultContext.Set(req, mux.ContextKey(0), vars) // mux.ContextKey exported
return req
}
Это решение не работает, поскольку context.DefaultContext
и mux.ContextKey
больше не существует.
Другим предлагаемым решением было бы изменить ваш код, чтобы функции запроса также принимали map[string]string
в качестве третьего параметра. Другие решения включают фактически запуск сервера и построение запроса и отправку его непосредственно на сервер. По моему мнению, это приведет к поражению цели модульного тестирования, превратив его в функциональные тесты.
Учитывая тот факт, что связанный поток с 2013 года. Есть ли другие варианты?
ИЗМЕНИТЬ
Итак, я прочитал исходный код gorilla/mux
, и в соответствии с mux.go
функция mux.Vars()
определена здесь следующим образом:
// Vars returns the route variables for the current request, if any.
func Vars(r *http.Request) map[string]string {
if rv := context.Get(r, varsKey); rv != nil {
return rv.(map[string]string)
}
return nil
}
Значение varsKey
определяется как iota
здесь. Таким образом, ключевым значением является 0
. Я написал небольшое тестовое приложение, чтобы проверить это:
main.go
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/gorilla/context"
)
func main() {
r, _ := http.NewRequest("GET", "/test/abcd", nil)
vars := map[string]string{
"mystring": "abcd",
}
context.Set(r, 0, vars)
what := Vars(r)
for key, value := range what {
fmt.Println("Key:", key, "Value:", value)
}
what2 := mux.Vars(r)
fmt.Println(what2)
for key, value := range what2 {
fmt.Println("Key:", key, "Value:", value)
}
}
func Vars(r *http.Request) map[string]string {
if rv := context.Get(r, 0); rv != nil {
return rv.(map[string]string)
}
return nil
}
Что при запуске выдает:
Key: mystring Value: abcd
map[]
Заставляет меня задаться вопросом, почему тест не работает и почему прямой вызов mux.Vars
не работает.
Ответы
Ответ 1
Проблема, даже если вы используете 0
как значение для установки значений контекста, это не то же значение, что и mux.Vars()
. mux.Vars()
использует varsKey
(как вы уже видели), который имеет тип contextKey
, а не int
.
Конечно, contextKey
определяется как:
type contextKey int
что означает, что он имеет int в качестве основного объекта, но тип играет роль при сравнении значений в go, поэтому int(0) != contextKey(0)
.
Я не вижу, как вы могли бы обмануть муклизм или контекст горилл в возвращении ваших значений.
Сказав это, вам приходит в голову несколько способов проверить это (обратите внимание, что код ниже не проверен, я набрал его прямо здесь, поэтому могут быть некоторые глупые ошибки):
- Как кто-то предложил, запустите сервер и отправьте ему HTTP-запросы.
-
Вместо запуска сервера просто используйте gouilla mux Router в своих тестах. В этом случае у вас будет один маршрутизатор, который вы передадите на ListenAndServe
, но вы также можете использовать тот же экземпляр маршрутизатора в тестах и называть ServeHTTP
на нем. Маршрутизатор позаботится о настройке значений контекста, и они будут доступны в ваших обработчиках.
func Router() *mux.Router {
r := mux.Router()
r.HandleFunc("/employees/{1}", GetRequest)
(...)
return r
}
где-то в главной функции вы бы сделали что-то вроде этого:
http.Handle("/", Router())
и в ваших тестах вы можете:
func TestGetRequest(t *testing.T) {
r := http.NewRequest("GET", "employees/1", nil)
w := httptest.NewRecorder()
Router().ServeHTTP(w, r)
// assertions
}
-
Оберните обработчики, чтобы они принимали параметры URL в качестве третьего аргумента, и оболочка должна вызывать mux.Vars()
и передавать параметры URL-адреса обработчику.
С помощью этого решения у ваших обработчиков будет подпись:
type VarsHandler func (w http.ResponseWriter, r *http.Request, vars map[string]string)
и вам придется адаптировать вызовы к нему, чтобы он соответствовал интерфейсу http.Handler
:
func (vh VarsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
vh(w, r, vars)
}
Чтобы зарегистрировать обработчик, вы должны использовать:
func GetRequest(w http.ResponseWriter, r *http.Request, vars map[string]string) {
// process request using vars
}
mainRouter := mux.NewRouter().StrictSlash(true)
mainRouter.HandleFunc("/test/{mystring}", VarsHandler(GetRequest)).Name("/test/{mystring}").Methods("GET")
Какой из них вы используете, это вопрос личных предпочтений. Лично я, вероятно, поеду с вариантом 2 или 3, с небольшим преимуществом в сторону 3.
Ответ 2
gorilla/mux
SetURLVars
предоставляет функцию SetURLVars
для целей тестирования, которую вы можете использовать для введения своих ложных vars
.
func TestGetRequest(t *testing.T) {
t.Parallel()
r, _ := http.NewRequest("GET", "/test/abcd", nil)
w := httptest.NewRecorder()
//Hack to try to fake gorilla/mux vars
vars := map[string]string{
"mystring": "abcd",
}
// CHANGE THIS LINE!!!
r = mux.SetURLVars(r, vars)
GetRequest(w, r)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, []byte("abcd"), w.Body.Bytes())
}
Ответ 3
В golang у меня есть несколько иной подход к тестированию.
Я немного переписываю ваш код lib:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
startServer()
}
func startServer() {
mainRouter := mux.NewRouter().StrictSlash(true)
mainRouter.HandleFunc("/test/{mystring}", GetRequest).Name("/test/{mystring}").Methods("GET")
http.Handle("/", mainRouter)
err := http.ListenAndServe(":8080", mainRouter)
if err != nil {
fmt.Println("Something is wrong : " + err.Error())
}
}
func GetRequest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
myString := vars["mystring"]
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(myString))
}
И вот тест для него:
package main
import (
"io/ioutil"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestGetRequest(t *testing.T) {
go startServer()
client := &http.Client{
Timeout: 1 * time.Second,
}
r, _ := http.NewRequest("GET", "http://localhost:8080/test/abcd", nil)
resp, err := client.Do(r)
if err != nil {
panic(err)
}
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
assert.Equal(t, []byte("abcd"), body)
}
Я думаю, что это лучший подход - вы действительно тестируете то, что вы написали, так как его очень легко запустить/остановить слушателей в go!
Ответ 4
Я использую следующую вспомогательную функцию для вызова обработчиков из модульных тестов:
func InvokeHandler(handler http.Handler, routePath string,
w http.ResponseWriter, r *http.Request) {
// Add a new sub-path for each invocation since
// we cannot (easily) remove old handler
invokeCount++
router := mux.NewRouter()
http.Handle(fmt.Sprintf("/%d", invokeCount), router)
router.Path(routePath).Handler(handler)
// Modify the request to add "/%d" to the request-URL
r.URL.RawPath = fmt.Sprintf("/%d%s", invokeCount, r.URL.RawPath)
router.ServeHTTP(w, r)
}
Поскольку нет (простого) способа отменить регистрацию обработчиков HTTP, а несколько вызовов на http.Handle
для одного и того же маршрута не удастся. Поэтому функция добавляет новый маршрут (например, /1
или /2
), чтобы гарантировать, что путь уникален. Эта магия необходима для использования функции в нескольких unit test в том же процессе.
Чтобы проверить вашу GetRequest
-функцию:
func TestGetRequest(t *testing.T) {
t.Parallel()
r, _ := http.NewRequest("GET", "/test/abcd", nil)
w := httptest.NewRecorder()
InvokeHandler(http.HandlerFunc(GetRequest), "/test/{mystring}", w, r)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, []byte("abcd"), w.Body.Bytes())
}
Ответ 5
Проблема в том, что вы не можете установить vars.
var r *http.Request
var key, value string
// runtime panic, map not initialized
mux.Vars(r)[key] = value
Решение заключается в создании нового маршрутизатора при каждом тестировании.
// api/route.go
package api
import (
"net/http"
"github.com/gorilla/mux"
)
type Route struct {
http.Handler
Method string
Path string
}
func (route *Route) Test(w http.ResponseWriter, r *http.Request) {
m := mux.NewRouter()
m.Handle(route.Path, route).Methods(route.Method)
m.ServeHTTP(w, r)
}
В файле обработчика.
// api/employees/show.go
package employees
import (
"github.com/gorilla/mux"
)
func Show(db *sql.DB) *api.Route {
h := func(w http.ResponseWriter, r http.Request) {
username := mux.Vars(r)["username"]
// .. etc ..
}
return &api.Route{
Method: "GET",
Path: "/employees/{username}",
// Maybe apply middleware too, who knows.
Handler: http.HandlerFunc(h),
}
}
В ваших тестах.
// api/employees/show_test.go
package employees
import (
"testing"
)
func TestShow(t *testing.T) {
w := httptest.NewRecorder()
r, err := http.NewRequest("GET", "/employees/ajcodez", nil)
Show(db).Test(w, r)
}
Вы можете использовать *api.Route
везде, где требуется http.Handler
.
Ответ 6
Так как context.setVar
не является публичным из Gorilla Mux, и они не исправили эту проблему более чем за два года, я решил, что просто сделаю обходной путь для своего сервера, который получает переменную из заголовка, а не контекст, если var пуст. Поскольку var никогда не должен быть пустым, это не изменяет функциональности моего сервера.
Создайте функцию для получения mux.Vars
func getVar(r *http.Request, key string) string {
v := mux.Vars(r)[key]
if len(v) > 0 {
return v
}
return r.Header.Get("X-UNIT-TEST-VAR-" + key)
}
Тогда вместо
vars := mux.Vars(r)
myString := vars["mystring"]
Просто позвоните
myString := getVar("mystring")
Это означает, что в ваших модульных тестах вы можете добавить функцию
func setVar(r *http.Request, key string, value string) {
r.Header.Set("X-UNIT-TEST-VAR-"+key, value)
}
Затем сделайте свой запрос
r, _ := http.NewRequest("GET", "/test/abcd", nil)
w := httptest.NewRecorder()
setVar(r, "mystring", "abcd")