My last post I build up a basic REST API. I set up the server and routing in
the home page and then build out a CRUD interface for the server to interact
with. Before we go any further I want to get some basic testing set up. There
is more than one way to set up testing and today I want to explore
(apitest)[https://apitest.dev/].
"A simple and extensible testing Library for
Go. You can use apitest to simplify testing of REST services, HTTP handlers
and HTTP clients."
Well simple sounds good to me so lets get started. One of the things I found
right way is that I can test headers and body content pretty easily and that
there is a debug option. OK, there are three parts Configuration, Request and
Assertions.
- Configuration sets up the call:
- Request is where you define the test input.
- Assertions: thats what you expect back.
Before we start testing here is the top bits of my code:
package users
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"gorm.io/gorm"
)
// Contact is the strict for the contacts list.
type Contact struct {
gorm.Model
Name string `json:"name"`
Phone string `json:"phone"`
Email string `json:"email"`
}
// Contacts is a slice of Contact.
var Contacts []Contact
//GetContacts retrieves all contacts
func GetContacts(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Contacts)
}
OK so with a bit of context lets look at the first piece of code I'm going to test.
in my contacts_test.go file I set it up like this:
package users
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/steinfletcher/apitest"
)
func TestGetcontacts(t *testing.T) {
r := mux.NewRouter()
r.HandleFunc("/contacts", GetContact).Methods("GET")
ts := httptest.NewServer(r)
defer ts.Close()
res, err := http.Get(ts.URL + "/contacts")
if err != nil {
t.Errorf("Got an error: %s", err.Error())
}
if res.StatusCode != http.StatusOK {
t.Errorf("Expected %d, received %d", http.StatusOK, res.StatusCode)
}
}
Here is the code I'm testing again:
func GetContacts(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Contacts)
}
I mean pretty simple it sends back everything in the Contacts struct. In order
to get this up and running I populated a contacts variable with four names,
numbers and emails.
Contacts = append(Contacts, Contact{Name: "Dorin", Phone: "555-4385", Email: "Dorin@coolcat.com"})
Contacts = append(Contacts, Contact{Name: "CoCo", Phone: "444-4385", Email: "CoCo@coolcat.com"})
Contacts = append(Contacts, Contact{Name: "Lucky", Phone: "888-4385", Email: "Lucky@coolcat.com"})
Contacts = append(Contacts, Contact{Name: "Sidney", Phone: "999-4385", Email: "Sidney@coolcat.com"})
Wait a minute hold on. I thought we were using apitest? Oh we are going to use
apitest, but I've never used it before so I wanted to have something to compare
it with. Plus it was right after writing this bit that I went out and found
apitest. Here is what my Apitest test looks like.
t.Run("Get All Contacts", func(t *testing.T) {
apitest.New(). // this line starts a new test in the apitest framework
Handler(r). // remember we defined r to our mux router at the top of the function.
Get("/contacts"). // what I want to get
Expect(t). // THIS IS THE PIVOT POINT
Status(http.StatusOK). // this is what I want to get back
End() // this ends the statement
})
Yes I used a t.Run and just tacked this onto my first test. Mostly because I
wanted to have the two to compare. I wanted to see this thing fail so I
changed the StatusOK to StatusNotFound and sure enough the test failed. I've
seen it fail, and I've seen it work, life is good. Next seems logical to test
getting a single contact.
I can reuse a lot of the code from my last test I just need to get more
specific. The GetContact function returns one contact so I need to test not
only that it returns a contact, but that it doesn't return a contact if non
exists. Lets look at the test.
func TestGetcontact(t *testing.T) {
// The setup!
r := mux.NewRouter()
r.HandleFunc("/contacts/{name}", GetContact).Methods("GET")
ts := httptest.NewServer(r)
defer ts.Close()
t.Run("Not Found", func(t *testing.T) { // yeah testing name not found!
apitest.New().
Handler(r).
Get("/contacts/DoesNotExist"). // asking for a name that's not there
Expect(t).
Status(http.StatusNotFound).
End()
})
This test fails -> expected: 404 || actual 200
I'm looking for StatusNotFound but I never told anybody to deal with the
headers. Had there been an error we would know about it but an empty json
package is just empty so I need to set the header when we don't find
anything. In my GetContact function I use an if statement to single out the
desired contact, encode it to json and send it back. All I have to do is add
a w.WriteHeader(http.StatusNotFound) right before the program exits and after
the if statement has returned. Check it out.
func GetContact(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
par := mux.Vars(r)
for _, target := range Contacts {
if target.Name == par["name"] { // where we single out the name
json.NewEncoder(w).Encode(target)
return // exit function if we found the name
}
}
w.WriteHeader(http.StatusNotFound) // if we got we the name wasn't found
json.NewEncoder(w).Encode(&Contact{} // so we write the status we want and
// send back the json.
}
that one line of code and now the test passes. What about when we find a
person?
t.Run("Found", func(t *testing.T) {
apitest.New().
Handler(r).
Debug().
Get("/contacts/Dorin").
Expect(t).
Status(http.StatusOK).
End()
})
}
Once again the test is going to fail because we don't have anybody in our
Contacts, so I pull down a name from the last test and use it.
Contacts = append(Contacts, Contact{Name: "Dorin", Phone: "555-4385", Email: "Dorin@coolcat.com"})
When I run this test again it passes! Now I'm getting back StatusOK and
presumably Dorin and his information. Lets just make sure that we did get back
Dorin and his phone and email information. Or even better I'm going to copy
over one more contact and I'm going to search for them, except this time I'm
going to check the body. So I make another contact named CoCo (Dorin and CoCo
are my dogs by the way). Then I set the test up like this:
t.Run("Found Check the Body", func(t *testing.T) {
apitest.New().
Handler(r).
Get("/contacts/CoCo").
Expect(t).
Assert(jsonpath.Contains(`$.phone`, "299-232-2385")). //here's the new bit
End()
})
Assert allows for just that and the jsonpath is a helper to make it easier to
check your json. The Contains() option allows you to pick any item in the json
string and check it. I checked the phone number because, why not.
Now the GET functions are tested lets move on to CreateContact.
A quick review of the code bit:
func CreateContact(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var contact Contact
_ = json.NewDecoder(r.Body).Decode(&contact)
Contacts = append(Contacts, contact)
json.NewEncoder(w).Encode(contact)
}
Here is the test code, most of this should be familiar. Since we are creating
no need for an in memory contact, we can just create one:
func TestCreatecontact(t *testing.T) {
r := mux.NewRouter()
r.HandleFunc("/contacts", CreateContact).Methods("POST")
ts := httptest.NewServer(r)
defer ts.Close()
apitest.New().
Handler(r).
Post("/contacts").
Header("Content-Type", "application/json").
JSON(`{"name": "X Tester", "phone": "555-5555", "email": "junik@gilskd"}`).
Expect(t).
Assert(jsonpath.Contains(`$.phone`, "555-5555")).
Status(http.StatusOK).
End()
}
In this test I added some json above the Expect(t), This will get sent with the
POST method. Then I Assert that it will give me back a StatusOK and the new
contact. That is exactly what we got back.
Now lets delete someone.
func DeleteContact(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
par := mux.Vars(r)
for idx, target := range Contacts {
if target.Name == par["name"] {
Contacts = append(Contacts[:idx], Contacts[idx+1:]...)
w.WriteHeader(http.StatusGone)
json.NewEncoder(w).Encode(Contacts)
break
}
}
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(Contacts)
}
I remeber this, we found the name, deleted it and tacked it back on the end
with the changes. OK, lets test:
func TestDeletecontact(t *testing.T) {
Contacts = append(Contacts, Contact{Name: "Dorin", Phone: "555-4385", Email: "Dorin@coolcat.com"})
r := mux.NewRouter()
r.HandleFunc("/contacts/{name}", DeleteContact).Methods("DELETE")
ts := httptest.NewServer(r)
defer ts.Close()
t.Run("Delete Existing contact Mocking", func(t *testing.T) {
apitest.New().
Handler(r).
Debug().
Delete("/contacts/Dorin").
Expect(t).
Status(http.StatusGone).
End()
})
This is pretty straight forward. I put Dorin into memory and then asked
DeleteContact to delete him. Now what happens if somebody tries to delete
someone that doesn't exits?
t.Run("No contact To Delete Mocking", func(t *testing.T) {
apitest.New().
Mocks().
Handler(r).
Debug().
Delete("/contacts/xxxNotacontact").
Expect(t).
Status(http.StatusNotFound).
End()
})
}
Sure enough when I run this test I get back StatusNotFound and the test passes.
Now there is only one function to test. Lets look at update.
func UpdateContact(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
par := mux.Vars(r)
fmt.Println(par)
json.NewEncoder(w).Encode(r)
for idx, target := range Contacts {
if target.Name != par["name"] {
fmt.Println("Trouble in Paradise")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(par)
return
}
if target.Name == par["name"] {
Contacts = append(Contacts[:idx], Contacts[idx+1:]...)
var contact Contact
_ = json.NewDecoder(r.Body).Decode(&contact)
contact.Name = par["name"]
fmt.Println("Can you believe we are in the if statement ", par["name"])
Contacts = append(Contacts, contact)
w.WriteHeader(http.StatusMovedPermanently)
json.NewEncoder(w).Encode(contact)
return
}
}
}
So update finds the name, deletes the contact and creates a new contact with the
updated information. We can test this like we tested create except here we will
need somebody in the contact list to make changes too. Here is my test code.
func TestUpdatecontact(t *testing.T) {
Contacts = append(Contacts, Contact{Name: "CoCo", Phone: "999-888-9999", Email: "Coco@onyxrocks.com"})
r := mux.NewRouter()
r.HandleFunc("/contacts/{name}", UpdateContact).Methods("PUT")
ts := httptest.NewServer(r)
defer ts.Close()
t.Run("contact Updated", func(t *testing.T) {
apitest.New().
Handler(r).
Observe().
Debug().
Put("contacts/CoCo"). // PUT instead of POST
JSON(`{"name": "CoCo", "phone": "555-5555", "email": "CoCo@Best.com"}`).
Expect(t).
Assert(jsonpath.Contains(`$.phone`, "555-5555")).
Status(http.StatusMovedPermanently).
End()
})
}
That rounds out my test suite for the time being. If there is any refactoring
to do it's always nice to do it under the protection of my tests. Once I've
looked over my code I'm going to jump back into my outline and I'll be back soon
with my next step. Thanks for reading and smile, it might hurt!
Tom Peltier
Top comments (0)