2015-12-23 2 views
18

Вот что я пытаюсь сделать:Модульное тестирование для функций, которые используют гориллы/параметры Мультиплексор URL

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) для блока тесты? Я нашел несколько дискуссий here, но предлагаемое решение больше не работает. Предлагаемое решение было:

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 года. Есть ли другие варианты?

EDIT

Так что я прочитал исходный код gorilla/mux, и в соответствии с mux.go функция mux.Vars() определяется here следующим образом:

// 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 определяется как iotahere , Таким образом, ключевым значением является 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 не работает.

ответ

17

Проблема в том, что при использовании 0 в качестве значения для установки значений контекста это не то же самое значение, которое читает mux.Vars(). mux.Vars() использует varsKey (как вы уже видели), который относится к категории contextKey, а не int.

Конечно, contextKey определяются как:

type contextKey int 

, что означает, что она имеет Int как базовый объект, но тип играет роль при сравнении значений в ходе, так что int(0) != contextKey(0).

Я не вижу, как вы могли бы обмануть муклизм или контекст горилл, чтобы вернуть свои ценности.


Это, как говорится, несколько способов проверить это приходит на ум (обратите внимание, что код ниже тестировался, я напечатал это прямо здесь, так что могут быть какие-то глупые ошибки):

  1. Как кто-то предложил, запустите сервер и отправьте ему HTTP-запросы.
  2. Вместо запуска сервера просто используйте 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 
    } 
    
  3. Wrap обработчики так, что они принимают параметры 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.

+0

Спасибо. Я думаю, что я буду использовать вариант 3. Однако 'gorilla/mux' должен исправить это, чтобы сделать модульный тест возможным. –

+0

Я обошел это, проверив, как горилла/мультиплексор провела тестирование в своем собственном пакете здесь, и поместила мое собственное утверждение внутри обработчика, где они проверяли значение 'vars': https://github.com/gorilla/mux /blob/master/context_native_test.go#L22 – xentek

1

В 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!

+0

Не является ли это функциональный тест, хотя? Я пытался выполнить единичный тест, в основном просто вызываю функцию, как и любую функцию с некоторыми значениями, которые вы создаете, и проверяете результат. –

+0

Это так. Но, на мой взгляд, для тестирования сетевого сервера достаточно функционального теста. В go, я делаю модульное тестирование в основном для сложной логики. –

+0

Хорошо. Спасибо за подсказку, но это на самом деле не отвечает на вопрос. Если бы я НЕ использовал URL-параметры с 'gorilla/mux', модульные тесты будут работать, и, по существу, я мог бы проводить тесты параллельно для каждой функции обработчика конечных точек без необходимости проходить через настоящий HTTP-сервер. Если бы я использовал только функциональные/приемочные тесты, я бы избежал реальной проблемы, с которой сталкивается 'gorilla/mux', и мои тесты занимают больше времени. Я планирую использовать как функциональные, так и модульные тесты в своем проекте. –

1

я использую следующую вспомогательную функцию для вызова обработчиков из модульных тестов:

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

вопрос вы не можете установить ВАР.

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 требуется.

-2

С context.setVar не является публичным из Gorilla Mux, и они не исправили эту проблему более чем за 2 года, я решил, что просто сделаю обходной путь для своего сервера, который получает переменную из заголовка вместо контекста если var пусто. Поскольку var никогда не должен быть пустым, это не изменяет функциональность моего сервера.

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

вам нужно изменить ваш тест:

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()) 
} 
Смежные вопросы