英文:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论