mux.Vars is not able to retrieve var from httpTest request

Issue

I’m writting a simple rest boiler plate for future projects, I’m currently working on some tests for my controller, I’m trying to retrieve a todo by it’s Id at /todo/{id}, here’s the handler.

func (t TodoController) GetById(w http.ResponseWriter, r *http.Request) {
    params := mux.Vars(r)
    id, err := strconv.Atoi(params["id"])

    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    todo, err := t.todoService.GetById(id)

    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    helpers.SendResponse(http.StatusOK, todo, w)
}

And here’s the test for this controller.

var (
    todoController TodoController
    recorder       *httptest.ResponseRecorder
    todos          []models.Todo
)

func setup() {
    todoController = *NewTodoController(services.NewTodoService())
    recorder = httptest.NewRecorder()
    todos = []models.Todo{
        {
            ID:        0,
            Todo:      "Buy milk",
            Completed: false,
        },
        {
            ID:        1,
            Todo:      "Buy cheese",
            Completed: false,
        },
        {
            ID:        2,
            Todo:      "Buy eggs",
            Completed: false,
        },
    }
}


func TestGetById(t *testing.T)  {
    // Arrange
    setup()
    request := httptest.NewRequest(http.MethodGet, "/todo/1", nil)
    var response models.Todo

    // Act
    todoController.GetById(recorder, request)
    result := recorder.Result()
    defer result.Body.Close()

    data, err := ioutil.ReadAll(result.Body)
    err = json.Unmarshal(data, &response)

    // Assert
    if err != nil {
        t.Errorf("Expected error to be nil but got %v", err)
    }

    assert.Equal(t, result.StatusCode, http.StatusOK, "Response should have been 200 Ok")
    assert.Equal(t, response, todos[1], "Response did not match the expected result")
}

It looks like when sending a request to /todo/1 mux is not able to retrieve the Id, so it end up returning a BadRequest error.

Here’s a link to this repo: https://github.com/Je12emy/rest_boiler

Solution

Mux provides two ways to inject request params – SetURLVars helper and using mux.Router wrapper instead of direct handler call.

SetURLVars does exactly what you want – injects otherwise not accessible parameters into HTTP request.

This method is simple, but has one issue though. Used URL and injected parameters are out of sync.

None forbids developers to write this code:

// we use 2 in request path
request := httptest.NewRequest(http.MethodGet, "/todo/2", nil)
// and we use 1 in request param
request = SetURLVars(request, map[string]string{"id":"1"})

This is not very clean testing practice.

We do not test if our var names in routing is correct. We can use item_id in router instead of id and test does not catch that.

Not only that, but we can use wrong path in router definition and map different handler. Client can delete order instead of todo item if we make that mistake.

That could be solved if we use our production Router in test.

Assume it is our production code:

func InitRouter(t TodoController) http.handler
    r := mux.NewRouter()
    r.HandleFunc("/todo/{id}", t.GetById).Methods(http.MethodGet)
    return r

In test, we can test GetById through router we created in InitRouter function:

func TestGetById(t *testing.T)  {
    // Arrange
    setup()
    request := httptest.NewRequest(http.MethodGet, "/todo/1", nil)
    var response models.Todo
    // added setup
    sut := InitRouter(todoController)


    // Act
    // changed act - calling GetById through production router
    sut.ServeHTTP(recorder, request)
    // no chnages after that
    result := recorder.Result()
    defer result.Body.Close()

    ...
}


Answered By – Dmitry Harnitski

Answer Checked By – Pedro (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.