0

I have a simple Gin server with one of the routes called /metadata.

What the handler does is it reads a file from the system, say /etc/myapp/metadata.json and returns the JSON in the response.

But when the file is not found, handler is configured to return following error.

500: metadata.json does not exists or not readable

On my system, which has the metadata.json file, the test passes. Here is the test function I am using:

package handlers_test

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "myapp/routes"

    "github.com/stretchr/testify/assert"
)

func TestMetadataRoute(t *testing.T) {
    router := routes.SetupRouter()

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/metadata", nil)
    router.ServeHTTP(w, req)

    assert.NotNil(t, w.Body)
    assert.Equal(t, 200, w.Code)
    assert.Contains(t, w.Body.String(), "field1")
    assert.Contains(t, w.Body.String(), "field2")
    assert.Contains(t, w.Body.String(), "field3")
    assert.Contains(t, w.Body.String(), "field4")
}

But on CI environment, the test would fail because it won't find metadata.json. And would return the configured error.

What can be done?

I have this handler:

func GetMetadata(c *gin.Context) {
    // read the info
    content, err := ioutil.ReadFile("/etc/myapp/metadata.json")
    if err != nil {
        c.JSON(http.StatusInternalServerError,
            gin.H{"error": "metadata.json does not exists or not readable"})
        return
    }

    // deserialize to json
    var metadata models.Metadata
    err = json.Unmarshal(content, &metadata)
    if err != nil {
        c.JSON(http.StatusInternalServerError,
            gin.H{"error": "unable to parse metadata.json"})
        return
    }

    c.JSON(http.StatusOK, metadata)
}
blackgreen
  • 34,072
  • 23
  • 111
  • 129
Santosh Kumar
  • 26,475
  • 20
  • 67
  • 118

1 Answers1

1

What Volker is suggesting is to use a package-level unexported variable. You give it a fixed default value, corresponding to the path you need in production, and then simply overwrite that variable in your unit test.

handler code:

var metadataFilePath = "/etc/myapp/metadata.json"

func GetMetadata(c *gin.Context) {
    // read the info
    content, err := ioutil.ReadFile(metadataFilePath)
    // ... rest of code
}

test code:

func TestMetadataRoute(t *testing.T) {
    metadataFilePath = "testdata/metadata_test.json"
    // ... rest of code
}

This is a super-simple solution. There are ways to improve on this, but all are variations of how to inject any variable in a Gin handler. For simple request-scoped configuration, what I usually do is to inject the variable into the Gin context. This requires slightly refactoring some of your code:

router setup code with middleware for production

func SetupRouter() {
    r := gin.New()
    r.GET("/metadata", MetadataPathMiddleware("/etc/myapp/metadata.json"), GetMetadata)
    // ... rest of code
}

func MetadataPathMiddleware(path string) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("_mdpath", path)
    }
}

handler code extracting the path from context:

func GetMetadata(c *gin.Context) {
    metadataFilePath := c.GetString("_mdpath")
    content, err := ioutil.ReadFile(metadataFilePath)
    // ... rest of code
}

test code which you should refactor to test the handler only (more details: How to unit test a Go Gin handler function?):

func TestMetadataRoute(t *testing.T) {
    // create Gin test context
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)

    // inject test value into context
    c.Set("_mdpath", "testdata/metadata_test.json")

    // just test handler, the passed context holds the test value
    GetMetadata(c)
   
    // ... assert
}

Note: setting context values with string keys is somewhat discouraged, however the Gin context accepts only string keys.

blackgreen
  • 34,072
  • 23
  • 111
  • 129