Automate testing for Golang Gin-gonic RESTful APIs
Fullstack Developer and Tech Author
Gin, a high-performance HTTP web framework written in Golang, provides features like routing and middleware out of the box. Gin helps developers to reduce boilerplate code and improve productivity - simplifying the process of building microservices.
In this tutorial, I will guide you through building a RESTful API with Golang using the Gin-gonic framework. I will also lead you through building a basic API to create, edit, delete, and retrieve a list of companies.
For the sake of simplicity, I won’t cover data persistence in this tutorial. Instead, you will use a dummy list of companies that you can update or delete from as you like. As simple as that might sound, it is enough to get you started with building a robust API and unit testing with Golang.
Prerequisites
You will need the following to get the most out of this tutorial:
- Go installed on your system: download instructions
- Basic knowledge of the Go programming language
- A CircleCI account
- A GitHub account
Getting started
To begin, use the terminal to go to your development folder. Create a new folder for the project using these commands:
mkdir golang-gin-company-api
cd golang-gin-company-api
These commands create and open a folder named golang-gin-company-api
.
Next, initialize a Go module within the project. Run:
go mod init golang-gin-company-api
This creates a go.mod
file where your project’s dependencies will be listed for tracking.
Installing the project’s dependencies
This project uses the Gin framework as an external dependency. To install the latest version of Gin and other dependencies, enter this command from the root of your project:
go get -u github.com/gin-gonic/gin github.com/stretchr/testify github.com/rs/xid
Once the installation process is successful, you have access to Gin. You also have access to these packages:
- Testify is one of the most popular testing packages for Golang.
- XID is a globally unique id generator library.
Creating the homepage
Now, create a file named main.go
within the root of the project. This will be the entry point of the application and will also house most of the functions that will be responsible for all functionalities. Open the new file and enter this content:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func HomepageHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message":"Welcome to the Tech Company listing API with Golang"})
}
func main() {
router := gin.Default()
router.GET("/", HomepageHandler)
router.Run()
}
This code imports Gin and a net/http package that provides HTTP client and server implementations. It creates a HomepageHandler()
method to handle responses on the homepage of your application.
The main()
function initializes a new Gin router, defines the HTTP verb for the homepage, and runs an HTTP server on the default port of 8080
by invoking the Run()
of the Gin instance.
Running the project
To run the project, enter the following command:
go run main.go
This command runs the application on the default port 8080
. Go to http://localhost:8080
to review it.
Now you can start implementing the logic for the API endpoints. For now, stop the application from running using CTRL+C. Then press Enter.
Creating a REST API
Before you go on, you need to define a data structure that will hold information about a company: the properties and fields. Each company will have an ID
, a Name
, the name of the CEO
, and Revenue
- the estimated annual revenue generated by the company.
Defining the company model
Use a Go struct to define this model. Within the main.go
file, declare the following struct:
type Company struct {
ID string `json:"id"`
Name string `json:"name"`
CEO string `json:"ceo"`
Revenue string `json:"revenue"`
}
To easily map each field to a specific name, specify the tags on each using backticks. This lets you send responses that fit into the JSON naming convention.
Defining a global variable
Next, define a global variable to represent companies and initialize the variable with dummy data. To the main.go
file, just after the Company
struct, add:
var companies = []Company{
{ID: "1", Name: "Dell", CEO: "Michael Dell", Revenue: "92.2 billion"},
{ID: "2", Name: "Netflix", CEO: "Reed Hastings", Revenue: "20.2 billion"},
{ID: "3", Name: "Microsoft", CEO: "Satya Nadella", Revenue: "320 million"},
}
Creating a new company
Next, define the required logic to create a new company. Create a new method within the main.go
file and call it *NewCompanyHandler*
. Use this code for it:
func NewCompanyHandler(c *gin.Context) {
var newCompany Company
if err := c.ShouldBindJSON(&newCompany); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
newCompany.ID = xid.New().String()
companies = append(companies, newCompany)
c.JSON(http.StatusCreated, newCompany)
}
This code snippet binds the incoming request body into a Company
struct instance and then specifies a unique ID
. It appends the newCompany
to the list of companies. If there is an error, it returns an error response, otherwise it returns a success confirmation.
Getting the list of companies
To retrieve the list of companies, define a *GetCompaniesHandler*
method:
func GetCompaniesHandler(c *gin.Context) {
c.JSON(http.StatusOK, companies)
}
This uses the c.JSON()
method to map the companies
array into JSON and return it.
Updating a company
To update the details of an existing company, define a method named *UpdateCompanyHandler*
with this content:
func UpdateCompanyHandler(c *gin.Context) {
id := c.Param("id")
var company Company
if err := c.ShouldBindJSON(&company); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
index := -1
for i := 0; i < len(companies); i++ {
if companies[i].ID == id {
index = 1
}
}
if index == -1 {
c.JSON(http.StatusNotFound, gin.H{
"error": "Company not found",
})
return
}
companies[index] = company
c.JSON(http.StatusOK, company)
}
This snippet uses the c.Param()
method to fetch the company’s unique id
from the request URL. It checks whether the record exists for the company and, if so, updates it.
Deleting a company
Create a *DeleteCompanyHandler*
method with this content:
func DeleteCompanyHandler(c *gin.Context) {
id := c.Param("id")
index := -1
for i := 0; i < len(companies); i++ {
if companies[i].ID == id {
index = 1
}
}
if index == -1 {
c.JSON(http.StatusNotFound, gin.H{
"error": "Company not found",
})
return
}
companies = append(companies[:index], companies[index+1:]...)
c.JSON(http.StatusOK, gin.H{
"message": "Company has been deleted",
})
}
Similar to the *UpdateCompanyHandler*
, the method from this snippet uses the unique identifier to target the details of the company that needs to be removed from the list. It deletes the company details, and returns a success confirmation.
Setting up an API route handler
Next, register all the endpoints and map them to the methods defined earlier. Update main()
as shown here:
func main() {
router := gin.Default()
router.GET("/", HomepageHandler)
router.GET("/companies", GetCompaniesHandler)
router.POST("/company", NewCompanyHandler)
router.PUT("/company/:id", UpdateCompanyHandler)
router.DELETE("/company/:id", DeleteCompanyHandler)
router.Run()
}
This would have been updated if you were using a code editor or IDE that supports automatic imports of packages. If you are not using that type of editor or IDE, make sure that the import
matches this snippet:
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/rs/xid"
)
Testing the application
Within the required methods defined and individual endpoints registered, go back to the terminal and run the application again using go run main.go
. This will start the application on port 8080
.
Creating a new company
Test this using Postman or your preferred API testing tool. Send an HTTP POST
request to http://localhost:8080/company
. Use the data below as the request payload:
{
"name": "Shrima Pizza",
"ceo": "Demo CEO",
"revenue": "300 million"
}
Retrieving the list of companies
To retrieve the list of companies, set an HTTP GET
request to http://localhost:8080/companies
.
Writing tests for the endpoints
Now you can focus on writing unit tests for all the methods created to handle the logic for your API endpoints.
Golang comes with a testing package that makes it easier to write tests. To begin, create a file named main_test.go
and populate it with this:
package main
import "github.com/gin-gonic/gin"
func SetUpRouter() *gin.Engine{
router := gin.Default()
return router
}
This is a method to return an instance of the Gin router. It will come in handy when testing other functions for each endpoint.
Note: Each test file within your project must end with _test.go
and each test method must start with a Test
prefix. This is a standard naming convention for a valid test.
Testing homepage response
In the main_test.go
file, define a *TestHomepageHandler*
method and use this code:
func TestHomepageHandler(t *testing.T) {
mockResponse := `{"message":"Welcome to the Tech Company listing API with Golang"}`
r := SetUpRouter()
r.GET("/", HomepageHandler)
req, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
responseData, _ := io.ReadAll(w.Body)
assert.Equal(t, mockResponse, string(responseData))
assert.Equal(t, http.StatusOK, w.Code)
}
This test script sets up a server using the Gin engine and issues a GET
request to the homepage /
. It then uses the assert
property from the testify package to check the status code and response payload.
Testing the create new company endpoint
To test the /company
endpoint for your API, create a *TestNewCompanyHandler*
method and use this code for it:
func TestNewCompanyHandler(t *testing.T) {
r := SetUpRouter()
r.POST("/company", NewCompanyHandler)
companyId := xid.New().String()
company := Company{
ID: companyId,
Name: "Demo Company",
CEO: "Demo CEO",
Revenue: "35 million",
}
jsonValue, _ := json.Marshal(company)
req, _ := http.NewRequest("POST", "/company", bytes.NewBuffer(jsonValue))
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
}
This code snippet issues a POST
request with a sample payload and checks whether the returned response code is 201
- StatusCreated
.
Testing the get companies endpoint
Next is the method to test GET /companies
resource. Define the *TestGetCompaniesHandler*
method with this:
func TestGetCompaniesHandler(t *testing.T) {
r := SetUpRouter()
r.GET("/companies", GetCompaniesHandler)
req, _ := http.NewRequest("GET", "/companies", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
var companies []Company
json.Unmarshal(w.Body.Bytes(), &companies)
assert.Equal(t, http.StatusOK, w.Code)
assert.NotEmpty(t, companies)
}
This code issues a GET
request to the /companies
endpoint and ensures that the returned payload is not empty. It also asserts that the status code is 200
.
Testing the update company endpoint
The last test is for the HTTP handler responsible for updating company details. In the main_test.go
file use this code snippet:
func TestUpdateCompanyHandler(t *testing.T) {
r := SetUpRouter()
r.PUT("/company/:id", UpdateCompanyHandler)
company := Company{
ID: `2`,
Name: "Demo Company",
CEO: "Demo CEO",
Revenue: "35 million",
}
jsonValue, _ := json.Marshal(company)
reqFound, _ := http.NewRequest("PUT", "/company/"+company.ID, bytes.NewBuffer(jsonValue))
w := httptest.NewRecorder()
r.ServeHTTP(w, reqFound)
assert.Equal(t, http.StatusOK, w.Code)
reqNotFound, _ := http.NewRequest("PUT", "/company/12", bytes.NewBuffer(jsonValue))
w = httptest.NewRecorder()
r.ServeHTTP(w, reqNotFound)
assert.Equal(t, http.StatusNotFound, w.Code)
}
This sends two HTTP PUT
requests to the company/:id
endpoint. One has a payload and a valid company ID and the other has an ID that does not exist. The valid call will return a successful response code while the invalid one responds with StatusNotFound
.
Update the import section within the main_test.go
file:
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/rs/xid"
"github.com/stretchr/testify/assert"
)
Running the test locally
Now, run the test by issuing this command:
go test
To disable the Gin debug logs and enable verbose mode, run the command with a -V
flag:
GIN_MODE=release go test -v
Automating the tests
Testing your software is important, but running your tests manually can be time consuming and prone to errors. You can automate your tests by creating a continuous integration pipeline on CircleCI. To do this, you will first need to sign up for a free CircleCI account.
To add the required configuration, create a folder called .circleci
, and in it, create a new file named config.yml
. Open the new file and paste this code into it:
version: "2.1"
orbs:
go: circleci/go@1.11.0
jobs:
build:
executor:
name: go/default
tag: "1.22"
steps:
- checkout
- go/load-cache
- go/mod-download
- go/save-cache
- go/test:
covermode: atomic
failfast: true
race: true
workflows:
main:
jobs:
- build
This script pulls in the Go orb for CircleCI. This orb allows common Go-related tasks (like installing Go, downloading modules and caching) that need to be completed. It then checks out of the remote repository, and issues the command to run your test.
Next, set up a repository on GitHub and link the project to CircleCI. Review Pushing your project to GitHub for instructions.
Connecting to CircleCI
Log into your CircleCI account. If you signed up with your GitHub account, all your repositories will be available on your project’s dashboard.
Click the Set Up Project button. You will be prompted about whether you have already defined the configuration file for CircleCI within your project. Enter the branch name (for this tutorial, we are using main
). Click the Set Up Project button to complete the process.
Click any job in the workflow to review its steps.
You can get further details of a job by clicking it - the Run tests
job for example.
There you have it!
Conclusion
With over 50k stars on GitHub, it’s clear that Gin is becoming a top choice for building efficient APIs among Golang developers.
In this tutorial, I have shown you how to build a REST API using Golang and Gin. I led you through writing a unit test for each endpoint, and setting up a continuous integration pipeline for it using GitHub and CircleCI. I hope you can apply what you have learned to your own team’s projects.
Explore the code for the sample project here on GitHub.