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 DELETEPackages to use for Unit Testing in Go

  1. 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 — 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.
NOTE: Go avoids the use of assertions.

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 —

  • Line No. 1- Please note the function’s name here. It starts with Test so that the testing package recognizes the test case and the next words in the camel case also starts with an uppercase. The function has a parameter which is a pointer to the testing package’s T variable which signifies that it is a test case.
  • Line No. 2- This creates a new request to the /entries endpoint.
  • Line No. 6- Creates a new recorder to record the response received by the entries endpoint.
  • Line No. 8- Hits the endpoint with the recorder and request.
  • Line No. 9- Checks if the response is 200 OK.
  • Line No. 10- Sends an error tagging as a test failure.
  • Line No. 15- Is an expected output from the endpoint.
  • Line No. 16- Check if the response is equal to expected.

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.

Thanks for reading ❤

If you liked this post, share it with all of your programming buddies!

Follow me on Facebook | Twitter

Learn More

Learn How To Code: Google’s Go (golang) Programming Language

Go: The Complete Developer’s Guide (Golang)

Build Realtime Apps | React Js, Golang & RethinkDB

Golang Tutorial: Learn Golang by Examples

Build a chat app with Go

Google’s Go Essentials For Node.js / JavaScript Developers

Test Automation Using Pytest and Selenium WebDriver

Top 5 Java Test Frameworks for Automation in 2019

Learn Selenium Automation with Robot Framework from scratch

Building A REST API With MongoDB, Mongoose, And Node.js

Building REST API with Nodejs / MongoDB /Passport /JWT



Suggest:

Introduction to Go for PHP Developers

Introduction to Java Stream API

JWT Authentication with ASP.NET WEB API

How to build a Laravel REST API with Test-Driven Development

Open sourcing apiron: A Python package for declarative RESTful API interaction

Priority Hints