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

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

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

问题

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

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

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

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

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

  1. package handlers_test
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. "myapp/routes"
  7. "github.com/stretchr/testify/assert"
  8. )
  9. func TestMetadataRoute(t *testing.T) {
  10. router := routes.SetupRouter()
  11. w := httptest.NewRecorder()
  12. req, _ := http.NewRequest("GET", "/metadata", nil)
  13. router.ServeHTTP(w, req)
  14. assert.NotNil(t, w.Body)
  15. assert.Equal(t, 200, w.Code)
  16. assert.Contains(t, w.Body.String(), "field1")
  17. assert.Contains(t, w.Body.String(), "field2")
  18. assert.Contains(t, w.Body.String(), "field3")
  19. assert.Contains(t, w.Body.String(), "field4")
  20. }

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

有什么解决办法吗?

我有这个处理程序:

  1. func GetMetadata(c *gin.Context) {
  2. // 读取信息
  3. content, err := ioutil.ReadFile("/etc/myapp/metadata.json")
  4. if err != nil {
  5. c.JSON(http.StatusInternalServerError,
  6. gin.H{"error": "metadata.json不存在或不可读"})
  7. return
  8. }
  9. // 反序列化为JSON
  10. var metadata models.Metadata
  11. err = json.Unmarshal(content, &metadata)
  12. if err != nil {
  13. c.JSON(http.StatusInternalServerError,
  14. gin.H{"error": "无法解析metadata.json"})
  15. return
  16. }
  17. c.JSON(http.StatusOK, metadata)
  18. }
英文:

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.

  1. 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:

  1. package handlers_test
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "testing"
  6. "myapp/routes"
  7. "github.com/stretchr/testify/assert"
  8. )
  9. func TestMetadataRoute(t *testing.T) {
  10. router := routes.SetupRouter()
  11. w := httptest.NewRecorder()
  12. req, _ := http.NewRequest("GET", "/metadata", nil)
  13. router.ServeHTTP(w, req)
  14. assert.NotNil(t, w.Body)
  15. assert.Equal(t, 200, w.Code)
  16. assert.Contains(t, w.Body.String(), "field1")
  17. assert.Contains(t, w.Body.String(), "field2")
  18. assert.Contains(t, w.Body.String(), "field3")
  19. assert.Contains(t, w.Body.String(), "field4")
  20. }

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:

  1. func GetMetadata(c *gin.Context) {
  2. // read the info
  3. content, err := ioutil.ReadFile("/etc/myapp/metadata.json")
  4. if err != nil {
  5. c.JSON(http.StatusInternalServerError,
  6. gin.H{"error": "metadata.json does not exists or not readable"})
  7. return
  8. }
  9. // deserialize to json
  10. var metadata models.Metadata
  11. err = json.Unmarshal(content, &metadata)
  12. if err != nil {
  13. c.JSON(http.StatusInternalServerError,
  14. gin.H{"error": "unable to parse metadata.json"})
  15. return
  16. }
  17. c.JSON(http.StatusOK, metadata)
  18. }

答案1

得分: 1

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

处理程序代码:

  1. var metadataFilePath = "/etc/myapp/metadata.json"
  2. func GetMetadata(c *gin.Context) {
  3. // 读取信息
  4. content, err := ioutil.ReadFile(metadataFilePath)
  5. // ... 其余代码
  6. }

测试代码:

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

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

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

  1. func SetupRouter() {
  2. r := gin.New()
  3. r.GET("/metadata", MetadataPathMiddleware("/etc/myapp/metadata.json"), GetMetadata)
  4. // ... 其余代码
  5. }
  6. func MetadataPathMiddleware(path string) gin.HandlerFunc {
  7. return func(c *gin.Context) {
  8. c.Set("_mdpath", path)
  9. }
  10. }

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

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

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

  1. func TestMetadataRoute(t *testing.T) {
  2. // 创建Gin测试上下文
  3. w := httptest.NewRecorder()
  4. c, _ := gin.CreateTestContext(w)
  5. // 在上下文中注入测试值
  6. c.Set("_mdpath", "testdata/metadata_test.json")
  7. // 只测试处理程序,传递的上下文包含测试值
  8. GetMetadata(c)
  9. // ... 断言
  10. }

注意:使用字符串键设置上下文值在某种程度上是不鼓励的,但是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:

  1. var metadataFilePath = "/etc/myapp/metadata.json"
  2. func GetMetadata(c *gin.Context) {
  3. // read the info
  4. content, err := ioutil.ReadFile(metadataFilePath)
  5. // ... rest of code
  6. }

test code:

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

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

  1. func SetupRouter() {
  2. r := gin.New()
  3. r.GET("/metadata", MetadataPathMiddleware("/etc/myapp/metadata.json"), GetMetadata)
  4. // ... rest of code
  5. }
  6. func MetadataPathMiddleware(path string) gin.HandlerFunc {
  7. return func(c *gin.Context) {
  8. c.Set("_mdpath", path)
  9. }
  10. }

handler code extracting the path from context:

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

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

  1. func TestMetadataRoute(t *testing.T) {
  2. // create Gin test context
  3. w := httptest.NewRecorder()
  4. c, _ := gin.CreateTestContext(w)
  5. // inject test value into context
  6. c.Set("_mdpath", "testdata/metadata_test.json")
  7. // just test handler, the passed context holds the test value
  8. GetMetadata(c)
  9. // ... assert
  10. }

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:

确定