Why is the response body empty when running a test of mux API?

1.6k views Asked by At

I am trying to build and test a very basic API in Go to learn more about the language after following their tutorial. The API and the four routes defined work in Postman and the browser, but when trying to write the test for any of the routes, the ResponseRecorder doesn't have a body, so I cannot verify it is correct.

I followed the example here and it works, but when I change it for my route, there is no response.

Here is my main.go file.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

// A Person represents a user.
type Person struct {
    ID        string    `json:"id,omitempty"`
    Firstname string    `json:"firstname,omitempty"`
    Lastname  string    `json:"lastname,omitempty"`
    Location  *Location `json:"location,omitempty"`
}

// A Location represents a Person's location.
type Location struct {
    City    string `json:"city,omitempty"`
    Country string `json:"country,omitempty"`
}

var people []Person

// GetPersonEndpoint returns an individual from the database.
func GetPersonEndpoint(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(req)
    for _, item := range people {
        if item.ID == params["id"] {
            json.NewEncoder(w).Encode(item)
            return
        }
    }
    json.NewEncoder(w).Encode(&Person{})
}

// GetPeopleEndpoint returns all people from the database.
func GetPeopleEndpoint(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(people)
}

// CreatePersonEndpoint creates a new person in the database.
func CreatePersonEndpoint(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(req)
    var person Person
    _ = json.NewDecoder(req.Body).Decode(&person)
    person.ID = params["id"]
    people = append(people, person)
    json.NewEncoder(w).Encode(people)
}

// DeletePersonEndpoint deletes a person from the database.
func DeletePersonEndpoint(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(req)
    for index, item := range people {
        if item.ID == params["id"] {
            people = append(people[:index], people[index+1:]...)
            break
        }
    }
    json.NewEncoder(w).Encode(people)
}

// SeedData is just for this example and mimics a database in the 'people' variable.
func SeedData() {
    people = append(people, Person{ID: "1", Firstname: "John", Lastname: "Smith", Location: &Location{City: "London", Country: "United Kingdom"}})
    people = append(people, Person{ID: "2", Firstname: "John", Lastname: "Doe", Location: &Location{City: "New York", Country: "United States Of America"}})
}

func main() {
    router := mux.NewRouter()
    SeedData()
    router.HandleFunc("/people", GetPeopleEndpoint).Methods("GET")
    router.HandleFunc("/people/{id}", GetPersonEndpoint).Methods("GET")
    router.HandleFunc("/people/{id}", CreatePersonEndpoint).Methods("POST")
    router.HandleFunc("/people/{id}", DeletePersonEndpoint).Methods("DELETE")
    fmt.Println("Listening on http://localhost:12345")
    fmt.Println("Press 'CTRL + C' to stop server.")
    log.Fatal(http.ListenAndServe(":12345", router))
}

Here is my main_test.go file.

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestGetPeopleEndpoint(t *testing.T) {
    req, err := http.NewRequest("GET", "/people", nil)
    if err != nil {
        t.Fatal(err)
    }

    // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(GetPeopleEndpoint)

    // Our handlers satisfy http.Handler, so we can call their ServeHTTP method
    // directly and pass in our Request and ResponseRecorder.
    handler.ServeHTTP(rr, req)

    // Trying to see here what is in the response.
    fmt.Println(rr)
    fmt.Println(rr.Body.String())

    // Check the status code is what we expect.
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }

    // Check the response body is what we expect - Commented out because it will fail because there is no body at the moment.
    // expected := `[{"id":"1","firstname":"John","lastname":"Smith","location":{"city":"London","country":"United Kingdom"}},{"id":"2","firstname":"John","lastname":"Doe","location":{"city":"New York","country":"United States Of America"}}]`
    // if rr.Body.String() != expected {
    //  t.Errorf("handler returned unexpected body: got %v want %v",
    //      rr.Body.String(), expected)
    // }
}

I appreciate that I am probably making a beginner mistake, so please take mercy on me. I have read a number of blogs testing mux, but can't see what I have done wrong.

Thanks in advance for your guidance.

UPDATE

Moving my SeeData call to init() resolved the body being empty for the people call.

func init() {
    SeedData()
}

However, I now have no body returned when testing a specific id.

func TestGetPersonEndpoint(t *testing.T) {
    id := 1
    path := fmt.Sprintf("/people/%v", id)
    req, err := http.NewRequest("GET", path, nil)
    if err != nil {
        t.Fatal(err)
    }

    // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response.
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(GetPersonEndpoint)

    // Our handlers satisfy http.Handler, so we can call their ServeHTTP method
    // directly and pass in our Request and ResponseRecorder.
    handler.ServeHTTP(rr, req)

    // Check request is made correctly and responses.
    fmt.Println(path)
    fmt.Println(rr)
    fmt.Println(req)
    fmt.Println(handler)

    // expected response for id 1.
    expected := `{"id":"1","firstname":"John","lastname":"Smith","location":{"city":"London","country":"United Kingdom"}}` + "\n"

    if status := rr.Code; status != http.StatusOK {
        message := fmt.Sprintf("The test returned the wrong status code: got %v, but expected %v", status, http.StatusOK)
        t.Fatal(message)
    }

    if rr.Body.String() != expected {
        message := fmt.Sprintf("The test returned the wrong data:\nFound: %v\nExpected: %v", rr.Body.String(), expected)
        t.Fatal(message)
    }
}

Moving my SeedData call to init() resolved the body being empty for the people call.

func init() {
    SeedData()
}

Creating a new router instance resolved the issue with accessing a variable on a route.

rr := httptest.NewRecorder()
router := mux.NewRouter()
router.HandleFunc("/people/{id}", GetPersonEndpoint)
router.ServeHTTP(rr, req)
1

There are 1 answers

4
Zuko On BEST ANSWER

I think its because your test isn't including the router hence the path variables aren't being detected. Here, try this

 // main.go
 func router() *mux.Router {
    router := mux.NewRouter()
    router.HandleFunc("/people", GetPeopleEndpoint).Methods("GET")
    router.HandleFunc("/people/{id}", GetPersonEndpoint).Methods("GET")
    router.HandleFunc("/people/{id}", CreatePersonEndpoint).Methods("POST")
    router.HandleFunc("/people/{id}", DeletePersonEndpoint).Methods("DELETE")
    return router
 }

and in your testcase, initiatize from the router method like below

 handler := router()
 // Our handlers satisfy http.Handler, so we can call their ServeHTTP method
 // directly and pass in our Request and ResponseRecorder.
 handler.ServeHTTP(rr, req)

And now if you try accessing the path variable id, it should be present in the map retured by mux since mux registered it when you initiatlized the Handler from mux Router instance returned from router()

 params := mux.Vars(req)
 for index, item := range people {
    if item.ID == params["id"] {
        people = append(people[:index], people[index+1:]...)
        break
    }
 }

Also like you mentioned, use the init function for one time setups.

 // main.go
 func init(){
    SeedData()
 }