如何测试依赖于外部文件的 Gin 处理程序?

huangapple go评论78阅读模式
英文:

How to test a handler in Gin which depends on external file?

问题

我有一个简单的Gin服务器,其中一个路由被称为/metadata

处理程序的功能是从系统中读取一个文件,比如/etc/myapp/metadata.json,然后将JSON返回给响应。

但是当找不到文件时,处理程序被配置为返回以下错误信息。

500: metadata.json不存在或不可读

在我的系统上,有metadata.json文件,测试通过。这是我正在使用的测试函数:

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

但是在CI环境中,测试将失败,因为它找不到metadata.json文件。并且会返回配置的错误。

有什么解决办法吗?

我有这个处理程序:

func GetMetadata(c *gin.Context) {
	// 读取信息
	content, err := ioutil.ReadFile("/etc/myapp/metadata.json")
	if err != nil {
		c.JSON(http.StatusInternalServerError,
			gin.H{"error": "metadata.json不存在或不可读"})
		return
	}

	// 反序列化为JSON
	var metadata models.Metadata
	err = json.Unmarshal(content, &metadata)
	if err != nil {
		c.JSON(http.StatusInternalServerError,
			gin.H{"error": "无法解析metadata.json"})
		return
	}

	c.JSON(http.StatusOK, metadata)
}
英文:

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

答案1

得分: 1

Volker建议使用包级别的未导出变量。您可以给它一个固定的默认值,对应于您在生产中需要的路径,然后在单元测试中简单地覆盖该变量。

处理程序代码:

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

func GetMetadata(c *gin.Context) {
    // 读取信息
    content, err := ioutil.ReadFile(metadataFilePath)
    // ... 其余代码
}

测试代码:

func TestMetadataRoute(t *testing.T) {
    metadataFilePath = "testdata/metadata_test.json"
    // ... 其余代码
}

这是一个非常简单的解决方案。有一些改进的方法,但都是如何在Gin处理程序中注入任何变量的变体。对于简单的请求范围配置,我通常会将变量注入到Gin上下文中。这需要稍微重构一些代码:

用于生产的路由设置代码和中间件:

func SetupRouter() {
    r := gin.New()
    r.GET("/metadata", MetadataPathMiddleware("/etc/myapp/metadata.json"), GetMetadata)
    // ... 其余代码
}

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

从上下文中提取路径的处理程序代码:

func GetMetadata(c *gin.Context) {
    metadataFilePath := c.GetString("_mdpath")
    content, err := ioutil.ReadFile(metadataFilePath)
    // ... 其余代码
}

测试代码,您应该重构为仅测试处理程序(更多细节:https://stackoverflow.com/questions/66952761/how-to-unit-test-a-go-gin-handler-function):

func TestMetadataRoute(t *testing.T) {
    // 创建Gin测试上下文
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)

    // 在上下文中注入测试值
    c.Set("_mdpath", "testdata/metadata_test.json")

    // 只测试处理程序,传递的上下文包含测试值
    GetMetadata(c)
   
    // ... 断言
}

注意:使用字符串键设置上下文值在某种程度上是不鼓励的,但是Gin上下文只接受字符串键。

英文:

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: https://stackoverflow.com/questions/66952761/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.

huangapple
  • 本文由 发表于 2022年6月28日 16:08:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/72782846.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定