Unit Testing for REST APIs in Go

Unit Testing for REST APIs in Go

Building RESTful APIs in different languages with different approaches and design patterns have always been as trending as being on a harder learning curve. This is due to focus on a lot of abstraction in code, the pain to get the project started and many more reasons. Upon that, to write test cases for the implemented services is also a pain in the neck.

Building RESTful APIs in different languages with different approaches and design patterns have always been as trending as being on a harder learning curve. This is due to focus on a lot of abstraction in code, the pain to get the project started and many more reasons. Upon that, to write test cases for the implemented services is also a pain in the neck.

Go gives you the privilege to write REST APIs in a very easy, elegant and concise way. In addition to that, Unit Testing in Go is also very easy and one command to hit to run the test cases.

As this article is just about writing unit test cases in Go, I would assume that you would know how to write REST implementation in Go. To get a deeper insight into this simple assignment, you can look into this example.

I have attached the postman collection for an easy import, sql dump and also has an attached readme file so that you can get started with the simple assignment.

So let's begin-

It would be better if we take into consideration an example to write the test cases. Let's say we want to have an Online Address Book application where you create an address book, in which you have the following fields —

package main

type entry struct {
    ID           int    `json:"id,omitempty"`
    FirstName    string `json:"first_name,omitempty"`
    LastName     string `json:"last_name,omitempty"`
    EmailAddress string `json:"email_address,omitempty"`
    PhoneNumber  string `json:"phone_number,omitempty"`
}

We are going to assume that we have endpoints for GetEntries , GetEntryByID , CreateEntryUpdateEntry andDeleteEntry .

GetEntries -> "/entries" -> Method **GET**

GetEntryByID -> "/entry?id=1234" -> Method **GET**

CreateEntry -> "/entry" -> Method **POST**

UpdateEntry -> "/entry" -> Method **PUT**

DeleteEntry -> "/entry" -> Method **DELETE**Packages to use for Unit Testing in Go

  1. [testing](https://golang.org/pkg/testing/ "testing") — This is an in-built Golang package which is used to implement and run unit/automated test cases. It is intended to work with go testcommand with optional parameters to accept the go file to be tested.
  2. [net/http/httptest](https://golang.org/pkg/net/http/httptest/ "net/http/httptest") — This is also an in-built Golang package which provides privileges to do HTTP testing. In our case, we would like to record response from the endpoints and do respective checks.

As this article is just about writing unit test cases in Go, I would assume that you would know how to write REST implementation in Go.### Writing Unit tests

Writing unit test cases in Go can be in same package or in different packages. There are 2 criteria through which the Go testing package identifies the test cases.

  • The file name should end with _test . For example — endpoints_test.go
  • The test cases function should start with Test word. For example -
func TestGetEntries(t *testing.T) { 
....
}

Writing Unit Test Cases for REST API endpoints

Let us take each endpoint one by one to see how can we test all the endpoints from the above example specified, i.e, GetEntriesGetEntryByID , GetEntryByIDNotFound , CreateEntry , EditEntry and DeleteEntry

Let us start with writing test cases for the following -

GetEntries Test Case—

func TestGetEntries(t *testing.T) {
    req, err := http.NewRequest("GET", "/entries", nil)
    if err != nil {
        t.Fatal(err)
    }
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(GetEntries)
    handler.ServeHTTP(rr, req)
    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.
    expected := `[{"id":1,"first_name":"Krish","last_name":"Bhanushali","email_address":"[email protected]","phone_number":"0987654321"},{"id":2,"first_name":"xyz","last_name":"pqr","email_address":"[email protected]","phone_number":"1234567890"},{"id":6,"first_name":"FirstNameSample","last_name":"LastNameSample","email_address":"[email protected]","phone_number":"1111111111"}]`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}

Note: The Github repository specified above does not contain a separate go file for each test case. I have specified all test cases in one go file.

Let us go through the get_entries_test.go line by line —

  • The file name should end with _test . For example — endpoints_test.go
  • The test cases function should start with Test word. For example -

    GetEntryByID Test Case-

func TestGetEntryByID(t *testing.T) {

    req, err := http.NewRequest("GET", "/entry", nil)
    if err != nil {
        t.Fatal(err)
    }
    q := req.URL.Query()
    q.Add("id", "1")
    req.URL.RawQuery = q.Encode()
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(GetEntryByID)
    handler.ServeHTTP(rr, req)
    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.
    expected := `{"id":1,"first_name":"Krish","last_name":"Bhanushali","email_address":"[email protected]","phone_number":"0987654321"}`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}

GetEntryByIDNotFound Test Case-

func TestGetEntryByIDNotFound(t *testing.T) {
    req, err := http.NewRequest("GET", "/entry", nil)
    if err != nil {
        t.Fatal(err)
    }
    q := req.URL.Query()
    q.Add("id", "123")
    req.URL.RawQuery = q.Encode()
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(GetEntryByID)
    handler.ServeHTTP(rr, req)
    if status := rr.Code; status == http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusBadRequest)
    }
}

CreateEntry Test Case-

func TestCreateEntry(t *testing.T) {

    var jsonStr = []byte(`{"id":4,"first_name":"xyz","last_name":"pqr","email_address":"[email protected]","phone_number":"1234567890"}`)

    req, err := http.NewRequest("POST", "/entry", bytes.NewBuffer(jsonStr))
    if err != nil {
        t.Fatal(err)
    }
    req.Header.Set("Content-Type", "application/json")
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(CreateEntry)
    handler.ServeHTTP(rr, req)
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }
    expected := `{"id":4,"first_name":"xyz","last_name":"pqr","email_address":"[email protected]","phone_number":"1234567890"}`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}

Let us go through create_entry_test.go line by line-

Line 1–8 are same as the above description where the input is a jsonStrwhich is a JSON string of the entry object and we create a new request with a new method POST .

Line 9- Sets the header with the Content-Type to be application/json

Line 9–22- Again, the same thing where the request is sent out and the response is being compared with the expected. If the request sent out is not 200 OK or the actual response is not equal to the expected response then the test case fails.

EditEntry Test Case-

func TestEditEntry(t *testing.T) {

    var jsonStr = []byte(`{"id":4,"first_name":"xyz change","last_name":"pqr","email_address":"[email protected]","phone_number":"1234567890"}`)

    req, err := http.NewRequest("PUT", "/entry", bytes.NewBuffer(jsonStr))
    if err != nil {
        t.Fatal(err)
    }
    req.Header.Set("Content-Type", "application/json")
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(UpdateEntry)
    handler.ServeHTTP(rr, req)
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }
    expected := `{"id":4,"first_name":"xyz change","last_name":"pqr","email_address":"[email protected]","phone_number":"1234567890"}`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}

EditEntry test case is the same as CreateEntry test case except the request method is PUT for EditEntry .

DeleteEntry test case-

func TestDeleteEntry(t *testing.T) {
    req, err := http.NewRequest("DELETE", "/entry", nil)
    if err != nil {
        t.Fatal(err)
    }
    q := req.URL.Query()
    q.Add("id", "4")
    req.URL.RawQuery = q.Encode()
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(DeleteEntry)
    handler.ServeHTTP(rr, req)
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusOK)
    }
    expected := `{"id":4,"first_name":"xyz change","last_name":"pqr","email_address":"[email protected]","phone_number":"1234567890"}`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v",
            rr.Body.String(), expected)
    }
}

DeleteEntry Test case is again the same as GetEntryByID test case except the request method is DELETE for DeleteEntry .

Running the test cases

Lets start the server first, so that our test cases hit the endpoints by go run api.go entry.go keeping in consideration the example specified.

To run all test cases in a test suite you can do the following-

go test -v

Running the test suite

To run one test case, you can simply use do the following-

go test -v -run <Test Function Name>

Running one test case i.e., TestGetEntries

Hence, we can come to the conclusion here that we now know how to write unit test cases for RESTful APIs in Go.

Hope this helps.

Angular 9 Tutorial: Learn to Build a CRUD Angular App Quickly

What's new in Bootstrap 5 and when Bootstrap 5 release date?

Brave, Chrome, Firefox, Opera or Edge: Which is Better and Faster?

How to Build Progressive Web Apps (PWA) using Angular 9

What is new features in Javascript ES2020 ECMAScript 2020

What the difference between REST API and RESTful API?

Representational state transfer (REST) is a style of software architecture. As described in a dissertation by Roy Fielding, REST is an "architectural style" that basically exploits the existing technology and protocols of the Web. RESTful is typically used to refer to web services implementing such an architecture.

Rest Assured Tutorial - Learn Rest API Testing (Automation) from Scratch

In this Rest Assured tutorial, I will explain what is API and API testing, API Automation, REST, what is the difference between SOAP and REST services, and how to test REST APIs with Rest Assured Library. Rest Assured is very popular in API Test Automation

What the difference between REST API and RESTful API?

Representational state transfer (REST) is a style of software architecture. As described in a dissertation by Roy Fielding, REST is an "architectural style" that basically exploits the existing technology and protocols of the Web. RESTful is typically used to refer to web services implementing such an architecture.