A logo showing the text blog.marcnuri.com
Español
Home»Go»Testing Go Gin Web Framework REST APIs with httptest

Recent Posts

  • Fabric8 Kubernetes Client 7.2 is now available!
  • Connecting to an MCP Server from JavaScript using AI SDK
  • Connecting to an MCP Server from JavaScript using LangChain.js
  • The Future of Developer Tools: Adapting to Machine-Based Developers
  • Connecting to a Model Context Protocol (MCP) Server from Java using LangChain4j

Categories

  • Artificial Intelligence
  • Front-end
  • Go
  • Industry and business
  • Java
  • JavaScript
  • Legacy
  • Operations
  • Personal
  • Pet projects
  • Tools

Archives

  • May 2025
  • April 2025
  • March 2025
  • February 2025
  • January 2025
  • December 2024
  • November 2024
  • August 2024
  • June 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
  • July 2022
  • June 2022
  • May 2022
  • March 2022
  • February 2022
  • January 2022
  • December 2021
  • November 2021
  • October 2021
  • September 2021
  • August 2021
  • July 2021
  • January 2021
  • December 2020
  • November 2020
  • October 2020
  • September 2020
  • August 2020
  • July 2020
  • June 2020
  • May 2020
  • February 2020
  • January 2020
  • December 2019
  • October 2019
  • September 2019
  • July 2019
  • March 2019
  • November 2018
  • July 2018
  • June 2018
  • May 2018
  • April 2018
  • March 2018
  • February 2018
  • December 2017
  • July 2017
  • January 2017
  • December 2015
  • November 2015
  • December 2014
  • March 2014
  • February 2011
  • November 2008
  • June 2008
  • May 2008
  • April 2008
  • January 2008
  • November 2007
  • September 2007
  • August 2007
  • July 2007
  • June 2007
  • May 2007
  • April 2007
  • March 2007

Testing Go Gin Web Framework REST APIs with httptest

2023-10-25 in Go tagged Go / Gin Web Framework / Testing / httptest / Test-Driven Development (TDD) by Marc Nuri | Last updated: 2023-10-25
Versión en Español

Introduction

The Gin Web Framework is one of the most popular Go frameworks for building RESTful APIs. When implementing your REST or HTTP endpoints, especially when following a test-driven development (TDD) approach, you'll most likely want to write unit tests to verify your endpoints.

The net/http/httptest package is a standard library that provides utilities to test HTTP servers and clients.

In this article, I'll show you how you can test your Gin-Gonic RESTful APIs using the httptest package to test the HTTP requests and responses.

The tested API

To be able to showcase the testing capabilities of the httptest package, I've implemented a simple RESTful API application using Gin. The application exposes a single endpoint in the root path / that allows for create, read, update, and delete (CRUD) operations for a fictional multipurpose object storage service. This is a simple demo project, objects handled by the API are stored in memory and are not persisted.

The following endpoints are exposed:

  • GET /:
    • Returns a list of all the items in the store for Accept: application/json requests
    • Returns the application name otherwise
  • POST /: Creates a new item and assigns a new id
  • PUT /:id: Upserts the provided item with the given id
  • DELETE /:id: Deletes the item with the given id

The following code snippet shows the Gin router setup function:

router.go
func SetupRouter() *gin.Engine {
  router := gin.Default()
  router.GET("/", addCommonHeaders, get, fallbackGet)
  router.POST("/", addCommonHeaders, post)
  router.PUT("/:id", addCommonHeaders, put)
  router.DELETE("/:id", addCommonHeaders, remove)
  return router
}

You can find the complete router implementation at GitHub (router.go).

The SetupRouter function is then used by the application's main function to start the server:

main.go
err := router.SetupRouter().Run("0.0.0.0:8080")

However, the SetupRouter function will also be used by the tests to record responses from the router. Let's check it out.

How to write a simple test

The following code snippet shows the complete test for the GET / endpoint when dealing with a request with no Accept header. In this case, the application should return a 200 status code, the application name in the body, and some standard headers.

router_test.go
func TestFallbackGet(t *testing.T) {
  router := SetupRouter()
  recorder := httptest.NewRecorder()
  router.ServeHTTP(recorder, httptest.NewRequest("GET", "/", nil))
  t.Run("Returns 200 status code", func(t *testing.T) {
    if recorder.Code != 200 {
      t.Error("Expected 200, got ", recorder.Code)
    }
  })
  t.Run("Returns app name", func(t *testing.T) {
    if recorder.Body.String() != "\"Cocktail service\"" {
      t.Error("Expected '\"Cocktail service\"', got ", recorder.Body.String())
    }
  })
  t.Run("Returns Server header", func(t *testing.T) {
    if recorder.Header().Get("Server") != "gin-gonic/1.33.7" {
      t.Error("Expected 'gin-gonic/1.33.7', got ", recorder.Header().Get("Server"))
    }
  })
}

We start the test implementation by creating a new Gin router by invoking the application's SetupRouter function. Then, we create a new httptest.Recorder. We will use it to capture the response from the router. Finally, we invoke the router's ServeHTTP function passing the recorder and a new httptest.Request with the GET / path and no body. This is the Action section of the test, which consists of two parts:

Generating the request using httptest.NewRequest

httptest.NewRequest("GET", "/", nil)

The first argument is the HTTP method, the second one is the path, and the third one is the request body.

Invoking the router's ServeHTTP function

router.ServeHTTP(recorder, request)

The first argument is the recorder we previously created, and the second one is the request.

The test implementation continues with multiple subtests to verify the different parts of the response. Let's analyze them.

Verifying the status code

The first assertion in the TestFallbackGet test verifies that the status code returned by the router is 200.

We achieve this by checking the value of the recorder.Code property which contains the HTTP status code of the response. Since we're using vanilla Go to perform the code assertion, we compare manually the value with the expected one.

router_test.go
if recorder.Code != 200 {
  t.Error("Expected 200, got ", recorder.Code)
}

If the assertion fails, we use the t.Error function to report the error.

Verifying the response body

The next assertion in the TestFallbackGet test verifies that the server response body contains the application name (Cocktail service).

Since this is a simple plain text literal, we can use the recorder.Body.String() function to get the response body as a string.

router_test.go
if recorder.Body.String() != "\"Cocktail service\"" {
  t.Error("Expected '\"Cocktail service\"', got ", recorder.Body.String())
}

Then, we compare the value with the expected one just like we did with the status code and report the error if the assertion fails.

Verifying the response headers

The last assertion in the TestFallbackGet test verifies that the server response contains the Server header with the engine and version of the application.

We manually set this header in the addCommonHeaders function of the application. The addCommonHeaders function is a handler added to all the routes of the application to set some response headers that are common to all endpoints:

router.go
func addCommonHeaders(c *gin.Context) {
  c.Header("Cache-Control", "no-cache, no-store")
  c.Header("Server", "gin-gonic/1.33.7")
}

To verify the header is present in the response, we use the recorder.Header().Get("Server") function to get its value.

router_test.go
if recorder.Header().Get("Server") != "gin-gonic/1.33.7" {
  t.Error("Expected 'gin-gonic/1.33.7', got ", recorder.Header().Get("Server"))
}

Then, we compare the value with the expected one just like we did with the status code and the response body, and report the error if the assertion fails.

These are very simple test scenarios that verify the basic components of the response. However, we can use the same approach to test more complex scenarios. Let's continue with more complex tests to completely verify the behavior of our RESTful API.

Dealing with more complex scenarios

The TestFallbackGet test we've seen in the previous section is a very simple test that verifies the basic components of the response for a simple HTTP GET request. However, we can use the same approach to test more complex scenarios such as different HTTP methods, headers, request bodies, response bodies, and so on.

Crafting a request

The tested application has a single endpoint that supports multiple HTTP methods. One of the accepted methods is POST. When performing a POST request, the application expects a JSON body containing an object and also a header with the Content-Type set to application/json.

We can easily use the httptest package to craft a request with these requirements:

request := httptest.NewRequest("POST", "/", strings.NewReader(`{
	"name": "Bloody Mary",
	"rating": 1
}`))
request.Header.Add("Content-Type", "application/json")

In the first line, we create a new httptest.Request with a POST method to the / path and a JSON body containing an object. To create the body, we use the strings.NewReader function to create a new io.Reader from a string literal. The string literal contains the JSON representation of the object we want to send to the server.

The next line adds the Content-Type header to the request using the Request.Header.Add function.

This request can now be used to invoke the router's ServeHTTP function and record the response.

router.ServeHTTP(recorder, request)

Verifying the body for a JSON response

When performing a POST request with a JSON body containing an object, the application will automatically assign an id to the object and return it in the response body. This and other scenarios are tested in the TestPostValid test.

The following code snippet shows the subtest that verifies that the returned object contains the generated id:

router_test.go
t.Run("Returns saved object with id", testCase(func(t *testing.T, c *context) {
  c.router.ServeHTTP(c.recorder, reqBuilder())
  var body map[string]interface{}
  json.Unmarshal(c.recorder.Body.Bytes(), &body)
  matched, _ := regexp.MatchString(
    "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
    body["id"].(string),
  )
  if !matched {
    t.Error("Expected object with id, got ", body)
  }
}))

In this case, we're using the json.Unmarshal function to parse the response body into a map[string]interface{}. Then, we use a regular expression to verify that the id property of the object matches the format of a UUID. We leverage the regexp.MatchString function to perform the regular expression match.

The router_test.go file contains a complete test suite that verifies the entire behavior of the application (and 100% code coverage). Please make sure to check it out.

Conclusion

In this article, we've seen how to test a Gin Web Framework RESTful API using the net/http/httptest package. We began by defining the application behavior. Then, we implemented some tests to verify the different components of the response to a simple HTTP GET request. Finally, we saw how to test more complex scenarios such as different HTTP methods, headers, request bodies, response bodies, and so on. By following these testing practices, you can ensure the robustness and reliability of your APIs.

You can find the full source code for this post at GitHub.

Happy testing!

Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Post navigation
Getting started with Testcontainers for GoHow to set up and tear down unit tests in Go
© 2007 - 2025 Marc Nuri