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.

go rest api testing

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

What is REST API? An Overview | Liquid Web

What is REST? The REST acronym is defined as a “REpresentational State Transfer” and is designed to take advantage of existing HTTP protocols when used

How to Do API Testing?

API endpoint when you send the get request to that URL it returns the JSON response. In this article, I am going to use postman assertions for all the examples since it is the most popular tool. But this article is not intended only for the postman tool.

Consume Web API Post method in ASP NET MVC | Calling Web API | Rest API Bangla Tutorial

LIKE | COMMENT | SHARE | SUBSCRIBE In this tutorial, I will discussed about how to consume Web API Get method and display records in the ASP.NET View. Here, ...

Consume Web API Get method in ASP NET MVC | Calling Web API | Rest API Bangla Tutorial

LIKE | COMMENT | SHARE | SUBSCRIBE In this tutorial, I will discussed about How to Consume Web API Get method in ASP NET MVC. Blog : http://aspdotnetexplorer...

Consume Web API Put method in ASP NET MVC | Calling Web API | Rest API Bangla Tutorial

LIKE | COMMENT | SHARE | SUBSCRIBE In this tutorial, we learned how to consume Web API Get and Post methods in the ASP.NET View. Here, we will see how to con...