Модульное тестирование функций, использующих параметры 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
в теле ответа.
Модульный тест для 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
не работает
6 ответов
Беда, даже когда вы используете 0
в качестве значения для установки значений контекста, это не то же значение, что mux.Vars()
читает. mux.Vars()
использует varsKey
(как вы уже видели), который имеет тип contextKey
и не int
,
Конечно, contextKey
определяется как:
type contextKey int
это означает, что он имеет int как базовый объект, но type играет роль при сравнении значений в go, поэтому int(0) != contextKey(0)
,
Я не понимаю, как вы могли бы обмануть гориллу или контекст в возвращении ваших значений.
При этом приходит на ум несколько способов проверить это (обратите внимание, что приведенный ниже код не проверен, я набрал его прямо здесь, поэтому могут быть некоторые глупые ошибки):
- Как кто-то предложил, запустите сервер и отправьте на него HTTP-запросы.
Вместо запуска сервера просто используйте gorilla 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.
Вам нужно изменить свой тест на:
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())
}
Я использую следующую вспомогательную функцию для вызова обработчиков из модульных тестов:
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
) чтобы путь был уникальным. Эта магия необходима для использования функции в нескольких модульных тестах в одном и том же процессе.
Чтобы проверить ваш 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())
}
В Голанге у меня немного другой подход к тестированию.
Я немного переписал твой код 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)
}
Я думаю, что это лучший подход - вы действительно тестируете то, что написали, поскольку очень легко запускать / останавливать слушателей на ходу!
Проблема в том, что вы не можете установить переменные.
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
нужно.
Поскольку context.setVar
не является общедоступным из Gorilla Mux, и они не исправили эту проблему более 2 лет, я решил, что просто сделаю обходной путь для моего сервера, который получает переменную из заголовка вместо контекста, если переменная пуста. Поскольку переменная никогда не должна быть пустой, это не меняет функциональность моего сервера.
Создать функцию для получения 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")