Подтвердить что ты не робот

Тестирование модулей для функций, которые используют параметры 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 не работает.

4b9b3361

Ответ 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")