Testing Go Gin Web Framework REST APIs with httptest
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
- Returns a list of all the items in the store for
POST /
: Creates a new item and assigns a newid
PUT /:id
: Upserts the provided item with the givenid
DELETE /:id
: Deletes the item with the givenid
The following code snippet shows the Gin router setup function:
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:
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.
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.
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.
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:
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.
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:
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!