DEV Community

loading...
Cover image for PART II First tests

PART II First tests

AdrnlnJnky
I am a Veteran outdoor leadership mentor and wilderness guide with a coding addiction. Looking to be a full time coder.
・7 min read

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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"})

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.
}

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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"})

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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

}

Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)