Golang: Unit tests with test gin context

Let’s explore why you should never miss automation tests and how to get started.
Jul 25 2022 · 5 min read

Introduction 

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.


Should you write unit tests or E2E tests for your project?

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.

What is a unit test?

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.

Why should you write unit tests?

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.

What is the E2E test?

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.

Why should you write E2E tests?

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.


How to get started writing unit tests for gin handlers in Golang?

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!


Create a test Gin context

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,

  • gin.SetMode(gin.TestMode) — sets gin mode to test
  • gin.CreateTestContext(w) — Create test gin context for running tests, which will have httptest.NewRecorder as an argument

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.


Preparing Requests

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.

GET

Passing Path Params


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.

Passing Query params

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()
}

Full code

// 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().

POST

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

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

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
}

Terminating Thoughts

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!!

Similar Articles


nidhi-d image
Nidhi Davra
Web developer@canopas | Gravitated towards Web | Eager to assist


nidhi-d image
Nidhi Davra
Web developer@canopas | Gravitated towards Web | Eager to assist

contact-footer
Say Hello!
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.