While listening to the word test, junior developers might get too many things in their minds. Most of the time they skip tests intentionally thinking of it as a time-wasting thing.
Of course! Testing can be time-consuming sometimes, but it can be a life savior proportionally.
It’s always good to correct your mistake before anyone else does.
Today we will explore everything you need to know about testing with Golang and Gin.
The answer is it depends.
Testing can be of any type(whichever your preference) like unit testing, E2E testing, integration testing, etc…
Ultimately the responsibility of tests is to verify the current behavior of the code to prevent future regressions.
It’s a block of code written to verify the behavior of a small piece of code. In Golang, this would mostly be a function or class.
A unit test checks the working of individual units(function or class) and exposes hidden vulnerabilities before it gets deployed at production.
“If you choose strong bricks, the wall will be stronger only”
Consider units of your code as bricks and full code as a wall.
Units are the basic building blocks of your code.
Thus, the entire system will only be able to work well if the individual parts are working well.
It’s also a block of code, which will test the code thoroughly(from head to tail), ultimately involving more than one unit like functions or classes.
For example, for writing an E2E test of an API, you need to invoke its endpoint itself with the required request data.
E2E testing plays an important role in ensuring that application users receive provides quality user experience.
As the unit test already verifies the behavior of individual units, the E2E test verifies whether units are combined properly to make the system work.
Unit tests sit above E2E tests, if you think a behavior is verified successfully with unit tests, E2E tests can be optional.
According to the use case, both can be mixed too.
To get started with unit tests, one needs to call the unit of code(function or sometimes a class) with the required requested data and observe its output.
It’s easy when you have simple functions like,
func sum(a int, b int) int {
return a + b
}
But it becomes a bit difficult to test a block of code that is directly attached to an endpoint and takes URL params, query params, request body, or headers as input.
However, it’s not impossible at all!
In this article, we will discuss how to test gin handlers(functions) that are using context, with the help of test gin context.
Let’s get started!
Consider a simple function below, for which you want to write a test.
func HelloWorld(c *gin.Context) {
text := c.DefaultQuery("txt", "Hey Go dev!!")
c.JSON(http.StatusOK, gin.H{"greeting": text})
}
// endpoint
r.GET("/hello", HelloWorld)
The argument of the HelloWorld function is a Gin context, which can’t be passed as simply as we pass int or string 😨 as an argument.
We have to prepare the test gin context to test our HelloWorld function, as it’s a gin handler.
func GetTestGinContext() *gin.Context {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
Header: make(http.Header)
}
return ctx
}
The above code is creating a test gin context, with which we can play while testing our code 😎! Here,
You can explore more about httptest. Here, httptest.NewRecorder() will be responsible for writing the response to HelloWorld().
Here, ctx.Request is configuring HTTP header, that is wrapped inside context.
Yay!! you just created a mock of Gin context for running tests.
As we can have HTTP requests with different request methods, let’s configure them separately.
The mocked gin context from the previous step will be passed as an argument to mock the requests below.
func MockJsonGet(c *gin.Context) {
c.Request.Method = "GET"
c.Request.Header.Set("Content-Type", "application/json")
c.Set("user_id", 1)
// set path params
c.Params = []gin.Param{
{
Key: "id",
Value: "1",
},
}
}
c.Params is used to provide path params to the function.
MockJsonGet Will wrap get request data into context.
For ex., if an endpoint is like r.get("/users/:id”, GetUserId)
, the id must be there when the function is called.
But as we're not invoking the endpoint, we can pass c.Params along with needed key-value pairs as shown in the above code.
For query params, we need to modify the context mocking a bit by adding URL key values in c.Request as shown in the below snippet.
// mock gin context
func GetTestGinContext() *gin.Context {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
Header: make(http.Header),
URL: &url.URL{},
}
return ctx
}
// mock GET request
func MockJsonGet(c *gin.Context) {
c.Request.Method = "GET"
c.Request.Header.Set("Content-Type", "application/json")
c.Set("user_id", 1)
// set query params
u := url.Values{}
u.Add("skip", "5")
u.Add("limit", "10")
c.Request.URL.RawQuery = u.Encode()
}
// code
func GetUserId(c *gin.Context) {
fmt.Println(c.Query("foo")) //will print "bar" while running test
fmt.Println(c.Param("id")) // will print "1" while running test
id, _ := strconv.Atoi(c.Param("id"))
c.JSON(http.StatusOK, id)
}
// test
func TestGetUserId(t *testing.T) {
w := httptest.NewRecorder()
ctx := GetTestGinContext(w)
//configure path params
params := []gin.Param{
{
Key: "id",
Value: "1",
},
}
// configure query params
u := url.Values{}
u.Add("foo", "bar")
MockJsonGet(ctx, params, u)
GetUserId(ctx)
assert.EqualValues(t, http.StatusOK, w.Code)
got, _ := strconv.Atoi(w.Body.String())
assert.Equal(t, 1, got)
}
//mock gin context
func GetTestGinContext(w *httptest.ResponseRecorder) *gin.Context {
gin.SetMode(gin.TestMode)
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
Header: make(http.Header),
URL: &url.URL{},
}
return ctx
}
//mock getrequest
func MockJsonGet(c *gin.Context, params gin.Params, u url.Values) {
c.Request.Method = "GET"
c.Request.Header.Set("Content-Type", "application/json")
c.Set("user_id", 1)
// set path params
c.Params = params
// set query params
c.Request.URL.RawQuery = u.Encode()
}
In MockJsonGet, query and path params are optional.
You will see, Query params and path params value printed inside GetUserId().
For all request types, the gin test context remains the same.
func MockJsonPost(c *gin.Context, content interface{}) {
c.Request.Method = "POST"
c.Request.Header.Set("Content-Type", "application/json")
c.Set("user_id", 1)
jsonbytes, err := json.Marshal(content)
if err != nil {
panic(err)
}
// the request body must be an io.ReadCloser
// the bytes buffer though doesn't implement io.Closer,
// so you wrap it in a no-op closer
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonbytes))
}
Here, the content will have the request JSON, first that will be converted to byte, and then it will be wrapped as the request body of test gin context.
PUT request mock will be a combination of GET and POST, as for updating something we will need its unique id and the updating details.
func MockJsonPut(c *gin.Context, content interface{}, params gin.Params) {
c.Request.Method = "PUT"
c.Request.Header.Set("Content-Type", "application/json")
c.Set("user_id", 1)
c.Params = params
jsonbytes, err := json.Marshal(content)
if err != nil {
panic(err)
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(jsonbytes))
}
I have taken path params only, optionally you can include query params too.
Delete will only require path param/query param, as mentioned below snippet.
func MockJsonDelete(c *gin.Context, params gin.Params) {
c.Request.Method = "DELETE"
c.Request.Header.Set("Content-Type", "application/json")
c.Set("user_id", 1)
c.Params = params
}
Using test gin context is effective when you want to test a gin handler, but don’t want to invoke the real endpoint(E2E test) while testing.
As always suggestions are more than welcome, please add comments if you have any.
keep testing for a better user experience!!