Unit Testing

Testing is done to ensure that our application behaves in a predictable manner. Writing Unit tests is the core responsibility of a developer and it is a part of development. While manual testing can be done, to ensure that our app works well, the need for unit testing will arise when we add features to an already built application and deploy it, only to find out that it broke some earlier functionality.

In one line testing is about ensuring our app doesn't break if a user gives a thoughtful but random input. We think of all such edge cases while testing the app and ensure that our app works fine.

A practical example: you wrote your first commandline Vi calculator and you are about to call a VC firm on your path to becoming filthy rich, but your team member finds out that if you divide 4 by 0, your app crashes.

"Of course nobody is going to divide by 0, don't they know math?", but they do

Go has a package called "testing" in the standard library.

While testing webapps, there are two aspects:

  1. Testing the look and feel.

  2. Testing if the pages displayed are with the valid statusCode.

  3. Testing if the app doesn't break when new functionality is added.

  4. The data IO works correctly.

The look and feel is a part of UI/UX and that can be done manually. Doesn't require us to write a single line of Go.

Validating the Status Code for edge cases

A real life scenario would be, what if a user who has a basic idea of HTTP decides to send you a OPTIONS /add/ request where you only accept either a GET or a POST.

Example: Testing the AddComment function

//AddCommentFunc will be used
func AddCommentFunc(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        r.ParseForm()
        text := r.Form.Get("commentText")
        id := r.Form.Get("taskID")

        idInt, err := strconv.Atoi(id)

        if (err != nil) || (text == "") {
            log.Println("unable to convert into integer")
            message = "Error adding comment"
        } else {
            username := sessions.GetCurrentUserName(r)
            err = db.AddComments(username, idInt, text)

            if err != nil {
                log.Println("unable to insert into db")
                message = "Comment not added"
            } else {
                message = "Comment added"
            }
        }

        http.Redirect(w, r, "/", http.StatusFound)

    }
}

This function will do the following: If the method is POST then take the comment content, extract the post ID and add the comment to the database.

We will test what happens when the user will send a HTTP GET request for the add comment function, which we do not support.

The test case function is:

func TestAddCommentWithWrongMethod(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(AddCommentFunc))
    defer ts.Close()
    req, err := http.NewRequest("GET", ts.URL, nil)
    if err != nil {
        t.Errorf("Error occured while constructing request: %s", err)
    }

    w := httptest.NewRecorder()
    AddCommentFunc(w, req)
    if w.Code != http.StatusBadRequest {
        t.Errorf("Actual status: (%d); Expected status:(%d)", w.Code, http.StatusBadRequest)
    }
}

func NewServer(handler http.Handler) *Server

NewServer starts and returns a new Server. The caller should call Close when finished, to shut it down. For testing, we do not need to run a server, we'll start a temporary server using NewServer every time that we need it.

Afer that, we have to send a HTTP request, which will be evaluated and a HTTP response will be sent by our handler.

In a regular HTTP application, we send a HTTP request with a URL to the server and the server takes the HTTP Request and generates a HTTP Response. While testing it, we have to mock the request and response. NewRequest() will create a request, and httptest.NewRecorder() will return an initialized ResponseRecorder.

func NewRecorder() *ResponseRecorder

[views]  go test . -v -run AddCommentWith
=== RUN   TestAddCommentWithWrongMethod
--- FAIL: TestAddCommentWithWrongMethod (0.00s)
           addViews_test.go:100: Actual status: (200); Expected status:(400)
FAIL
exit status 1
FAIL       github.com/thewhitetulip/Tasks/views       0.084s

We can see here that the actual status is 200, which is Okay. But this isn't what we want, for invalid status request, we should send a Bad Request.

We change the first line and add this, modify the rest of the function accordingly:

if r.Method != "POST" {
    log.Println(err)
    http.Redirect(w, r, "/", http.StatusBadRequest)
    return
}

If the method is anything but a POST, we'll send a error message.

[views]  go test . -v -run AddCommentWith
=== RUN   TestAddCommentWithWrongMethod
2016/08/27 14:19:33 <nil>
--- PASS: TestAddCommentWithWrongMethod (0.00s)
PASS
ok         github.com/thewhitetulip/Tasks/views       0.071s

Incremental testing

An application requires suit of test cases. A suit is going to have a lot of test cases. Suppose you already have a hundred test cases and you add TestValueAdd test case as a part of a new feature. While developing that feature. While you are testing if the TestValueAdd feature works correctly, you want to run only that one test case.

In such cases, you can run it as go test -run ValueAdd.

-run supports arguments as a regular expression.

After we are satisfied with this testcase, we have to run the entire test suit for integration testing.

Homework

Just as we tested the handler for wrong method, try running at able driven test for AddTask handler. You'll have to set up a dummy sqlite database, create few edge cases for inserting tasks, run the AddTask method and if the number of tasks isn't what you expect in the sqlite, then the test fails.

-Previous section -Next section

Last updated