TutorialsLast Updated Apr 16, 20249 min read

Automate testing for Golang Gin-gonic RESTful APIs

Olususi Oluyemi

Fullstack Developer and Tech Author

Developer C sits at a desk working on a beginning-level project.

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:

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.

Application homepage

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"
}

Create a new company

Retrieving the list of companies

To retrieve the list of companies, set an HTTP GET request to http://localhost:8080/companies.

Retrieve 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

Test out non verbose

To disable the Gin debug logs and enable verbose mode, run the command with a -V flag:

GIN_MODE=release go test -v

Testing output verbose

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.

Set up project

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.

Select config

Click any job in the workflow to review its steps.

Job steps

You can get further details of a job by clicking it - the Run tests job for example.

Pipeline output

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.


Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.

Copy to clipboard