测试Go的http.Request.FormFile函数吗?

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

Testing Go http.Request.FormFile?

问题

如何在测试端点时设置Request.FormFile?

部分代码:

  1. func (a *EP) Endpoint(w http.ResponseWriter, r *http.Request) {
  2. ...
  3. x, err := strconv.Atoi(r.FormValue("x"))
  4. if err != nil {
  5. a.ren.Text(w, http.StatusInternalServerError, err.Error())
  6. return
  7. }
  8. f, fh, err := r.FormFile("y")
  9. if err != nil {
  10. a.ren.Text(w, http.StatusInternalServerError, err.Error())
  11. return
  12. }
  13. defer f.Close()
  14. ...
  15. }

如何使用httptest库生成一个具有可以在FormFile中获取的值的POST请求?

英文:

How do I set the Request.FormFile when trying to test an endpoint?

Partial code:

  1. func (a *EP) Endpoint(w http.ResponseWriter, r *http.Request) {
  2. ...
  3. x, err := strconv.Atoi(r.FormValue("x"))
  4. if err != nil {
  5. a.ren.Text(w, http.StatusInternalServerError, err.Error())
  6. return
  7. }
  8. f, fh, err := r.FormFile("y")
  9. if err != nil {
  10. a.ren.Text(w, http.StatusInternalServerError, err.Error())
  11. return
  12. }
  13. defer f.Close()
  14. ...
  15. }

How do I use the httptest lib to generate a post request that has value that I can get in FormFile?

答案1

得分: 20

你不需要像其他答案建议的那样模拟完整的FormFile结构体。mime/multipart包实现了一个Writer类型,可以让你创建一个FormFile。从文档中可以看到:

> CreateFormFile是CreatePart的一个便利包装。它使用提供的字段名和文件名创建一个新的form-data头。

  1. func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error)

然后,你可以将这个io.Writer传递给httptest.NewRequest,它接受一个reader作为参数。

  1. request := httptest.NewRequest("POST", "/", myReader)

为了实现这一点,你可以将FormFile写入一个io.ReaderWriter缓冲区,或者使用io.Pipe。下面是一个完整的示例,使用了管道:

  1. func TestUploadImage(t *testing.T) {
  2. // 设置一个管道以避免缓冲
  3. pr, pw := io.Pipe()
  4. // 这个写入器将把我们传递给它的内容转换为多部分表单数据
  5. // 并将其写入我们的io.Pipe
  6. writer := multipart.NewWriter(pw)
  7. go func() {
  8. defer writer.Close()
  9. // 我们创建了一个名为'fileupload'的表单数据字段
  10. // 它返回另一个写入器来写入实际的文件
  11. part, err := writer.CreateFormFile("fileupload", "someimg.png")
  12. if err != nil {
  13. t.Error(err)
  14. }
  15. // https://yourbasic.org/golang/create-image/
  16. img := createImage()
  17. // Encode()接受一个io.Writer。
  18. // 我们传递了我们之前定义的multipart字段
  19. // 'fileupload',它反过来写入我们的io.Pipe
  20. err = png.Encode(part, img)
  21. if err != nil {
  22. t.Error(err)
  23. }
  24. }()
  25. // 我们从接收数据的管道中读取数据
  26. // 这个管道接收来自multipart写入器的数据
  27. // 而multipart写入器又接收来自png.Encode()的数据。
  28. // 我们有3个链接的写入器!
  29. request := httptest.NewRequest("POST", "/", pr)
  30. request.Header.Add("Content-Type", writer.FormDataContentType())
  31. response := httptest.NewRecorder()
  32. handler := UploadFileHandler()
  33. handler.ServeHTTP(response, request)
  34. t.Log("它应该以HTTP状态码200作为响应")
  35. if response.Code != 200 {
  36. t.Errorf("期望 %s,收到 %d", 200, response.Code)
  37. }
  38. t.Log("它应该在uploads文件夹中创建一个名为'someimg.png'的文件")
  39. if _, err := os.Stat("./uploads/someimg.png"); os.IsNotExist(err) {
  40. t.Error("期望文件'./uploads/someimg.png'存在")
  41. }
  42. }

这个函数利用了image包来动态生成一个文件,利用了可以将io.Writer传递给png.Encode的特性。同样地,你可以将你的multipart Writer传递给encoding/csv包中的NewWriter,以CSV格式生成字节,动态生成文件,而无需从文件系统中读取任何内容。

英文:

You don't need to mock the complete FormFile struct as suggested by the other answer. The mime/multipart package implements a Writer type that lets you create a FormFile. From the docs

> CreateFormFile is a convenience wrapper around CreatePart. It creates
> a new form-data header with the provided field name and file name.

  1. func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error)

Then, you can pass this io.Writer to httptest.NewRequest, which accepts a reader as an argument.

  1. request := httptest.NewRequest("POST", "/", myReader)

To do this, you can either write the FormFile to an io.ReaderWriter buffer or use an io.Pipe. Here is a complete example that makes use of pipes:

  1. func TestUploadImage(t *testing.T) {
  2. // Set up a pipe to avoid buffering
  3. pr, pw := io.Pipe()
  4. // This writer is going to transform
  5. // what we pass to it to multipart form data
  6. // and write it to our io.Pipe
  7. writer := multipart.NewWriter(pw)
  8. go func() {
  9. defer writer.Close()
  10. // We create the form data field 'fileupload'
  11. // which returns another writer to write the actual file
  12. part, err := writer.CreateFormFile("fileupload", "someimg.png")
  13. if err != nil {
  14. t.Error(err)
  15. }
  16. // https://yourbasic.org/golang/create-image/
  17. img := createImage()
  18. // Encode() takes an io.Writer.
  19. // We pass the multipart field
  20. // 'fileupload' that we defined
  21. // earlier which, in turn, writes
  22. // to our io.Pipe
  23. err = png.Encode(part, img)
  24. if err != nil {
  25. t.Error(err)
  26. }
  27. }()
  28. // We read from the pipe which receives data
  29. // from the multipart writer, which, in turn,
  30. // receives data from png.Encode().
  31. // We have 3 chained writers!
  32. request := httptest.NewRequest("POST", "/", pr)
  33. request.Header.Add("Content-Type", writer.FormDataContentType())
  34. response := httptest.NewRecorder()
  35. handler := UploadFileHandler()
  36. handler.ServeHTTP(response, request)
  37. t.Log("It should respond with an HTTP status code of 200")
  38. if response.Code != 200 {
  39. t.Errorf("Expected %s, received %d", 200, response.Code)
  40. }
  41. t.Log("It should create a file named 'someimg.png' in uploads folder")
  42. if _, err := os.Stat("./uploads/someimg.png"); os.IsNotExist(err) {
  43. t.Error("Expected file ./uploads/someimg.png' to exist")
  44. }
  45. }

This function makes use of the image package to generate a file dynamically taking advantage of the fact that you can pass an io.Writer to png.Encode. In the same vein, you could pass your multipart Writer to generate the bytes in a CSV format (NewWriter in package "encoding/csv"), generating a file on the fly, without needing to read anything from your filesystem.

答案2

得分: 8

如果你查看FormFile函数的实现,你会发现它读取了暴露的MultipartForm字段。

https://golang.org/src/net/http/request.go?s=39022:39107#L1249

  1. // FormFile返回提供的表单键的第一个文件。
  2. // 如果需要,FormFile会调用ParseMultipartForm和ParseForm。
  3. func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
  4. if r.MultipartForm == multipartByReader {
  5. return nil, nil, errors.New("http: multipart handled by MultipartReader")
  6. }
  7. if r.MultipartForm == nil {
  8. err := r.ParseMultipartForm(defaultMaxMemory)
  9. if err != nil {
  10. return nil, nil, err
  11. }
  12. }
  13. if r.MultipartForm != nil && r.MultipartForm.File != nil {
  14. if fhs := r.MultipartForm.File[key]; len(fhs) > 0 {
  15. f, err := fhs[0].Open()
  16. return f, fhs[0], err
  17. }
  18. }
  19. return nil, nil, ErrMissingFile
  20. }

在你的测试中,你应该能够创建一个multipart.Form的测试实例,并将其赋值给你的请求对象 - https://golang.org/pkg/mime/multipart/#Form

  1. type Form struct {
  2. Value map[string][]string
  3. File map[string][]*FileHeader
  4. }

当然,这将需要你使用一个真实的文件路径,这在测试的角度来看并不理想。为了解决这个问题,你可以定义一个接口来从请求对象中读取FormFile,并将一个模拟实现传递给你的EP结构体。

这是一个很好的帖子,其中包含一些关于如何做到这一点的示例: https://husobee.github.io/golang/testing/unit-test/2015/06/08/golang-unit-testing.html

英文:

If you have a look at the implementation of the FormFile function you'll see that it reads the exposed MultipartForm field.

https://golang.org/src/net/http/request.go?s=39022:39107#L1249

  1. // FormFile returns the first file for the provided form key.
  2. 1258 // FormFile calls ParseMultipartForm and ParseForm if necessary.
  3. 1259 func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
  4. 1260 if r.MultipartForm == multipartByReader {
  5. 1261 return nil, nil, errors.New("http: multipart handled by MultipartReader")
  6. 1262 }
  7. 1263 if r.MultipartForm == nil {
  8. 1264 err := r.ParseMultipartForm(defaultMaxMemory)
  9. 1265 if err != nil {
  10. 1266 return nil, nil, err
  11. 1267 }
  12. 1268 }
  13. 1269 if r.MultipartForm != nil && r.MultipartForm.File != nil {
  14. 1270 if fhs := r.MultipartForm.File[key]; len(fhs) > 0 {
  15. 1271 f, err := fhs[0].Open()
  16. 1272 return f, fhs[0], err
  17. 1273 }
  18. 1274 }
  19. 1275 return nil, nil, ErrMissingFile
  20. 1276 }

In your test you should be able to create a test instance of multipart.Form and assign it to your request object - https://golang.org/pkg/mime/multipart/#Form

  1. type Form struct {
  2. Value map[string][]string
  3. File map[string][]*FileHeader
  4. }

Of course this will require that you use a real filepath which isn't great from a testing perspective. To get around this you could define an interface to read FormFile from a request object and pass a mock implementation into your EP struct.

Here is a good post with a few examples on how to do this: https://husobee.github.io/golang/testing/unit-test/2015/06/08/golang-unit-testing.html

答案3

得分: 7

我将这些答案和其他内容整合到一个没有管道或goroutine的Echo示例中:

  1. func Test_submitFile(t *testing.T) {
  2. path := "testfile.txt"
  3. body := new(bytes.Buffer)
  4. writer := multipart.NewWriter(body)
  5. part, err := writer.CreateFormFile("object", path)
  6. assert.NoError(t, err)
  7. sample, err := os.Open(path)
  8. assert.NoError(t, err)
  9. _, err = io.Copy(part, sample)
  10. assert.NoError(t, err)
  11. assert.NoError(t, writer.Close())
  12. e := echo.New()
  13. req := httptest.NewRequest(http.MethodPost, "/", body)
  14. req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
  15. rec := httptest.NewRecorder()
  16. c := e.NewContext(req, rec)
  17. c.SetPath("/submit")
  18. if assert.NoError(t, submitFile(c)) {
  19. assert.Equal(t, 200, rec.Code)
  20. assert.Contains(t, rec.Body.String(), path)
  21. fi, err := os.Stat(expectedPath)
  22. if os.IsNotExist(err) {
  23. t.Fatal("Upload file does not exist", expectedPath)
  24. }
  25. assert.Equal(t, wantSize, fi.Size())
  26. }
  27. }
英文:

I combined these and other answers into an Echo example without pipes or goroutines:

  1. func Test_submitFile(t *testing.T) {
  2. path := "testfile.txt"
  3. body := new(bytes.Buffer)
  4. writer := multipart.NewWriter(body)
  5. part, err := writer.CreateFormFile("object", path)
  6. assert.NoError(t, err)
  7. sample, err := os.Open(path)
  8. assert.NoError(t, err)
  9. _, err = io.Copy(part, sample)
  10. assert.NoError(t, err)
  11. assert.NoError(t, writer.Close())
  12. e := echo.New()
  13. req := httptest.NewRequest(http.MethodPost, "/", body)
  14. req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
  15. rec := httptest.NewRecorder()
  16. c := e.NewContext(req, rec)
  17. c.SetPath("/submit")
  18. if assert.NoError(t, submitFile(c)) {
  19. assert.Equal(t, 200, rec.Code)
  20. assert.Contains(t, rec.Body.String(), path)
  21. fi, err := os.Stat(expectedPath)
  22. if os.IsNotExist(err) {
  23. t.Fatal("Upload file does not exist", expectedPath)
  24. }
  25. assert.Equal(t, wantSize, fi.Size())
  26. }
  27. }

答案4

得分: 6

通过结合之前的答案,以下代码对我起作用:

  1. filePath := "file.jpg"
  2. fieldName := "file"
  3. body := new(bytes.Buffer)
  4. mw := multipart.NewWriter(body)
  5. file, err := os.Open(filePath)
  6. if err != nil {
  7. t.Fatal(err)
  8. }
  9. w, err := mw.CreateFormFile(fieldName, filePath)
  10. if err != nil {
  11. t.Fatal(err)
  12. }
  13. if _, err := io.Copy(w, file); err != nil {
  14. t.Fatal(err)
  15. }
  16. // 在发送请求之前关闭写入器
  17. mw.Close()
  18. req := httptest.NewRequest(http.MethodPost, "/upload", body)
  19. req.Header.Add("Content-Type", mw.FormDataContentType())
  20. res := httptest.NewRecorder()
  21. // router 是 http.Handler 类型
  22. router.ServeHTTP(res, req)

请注意,这只是代码的翻译部分,不包括任何其他内容。

英文:

By combining the previous answers, this worked for me:

  1. filePath := "file.jpg"
  2. fieldName := "file"
  3. body := new(bytes.Buffer)
  4. mw := multipart.NewWriter(body)
  5. file, err := os.Open(filePath)
  6. if err != nil {
  7. t.Fatal(err)
  8. }
  9. w, err := mw.CreateFormFile(fieldName, filePath)
  10. if err != nil {
  11. t.Fatal(err)
  12. }
  13. if _, err := io.Copy(w, file); err != nil {
  14. t.Fatal(err)
  15. }
  16. // close the writer before making the request
  17. mw.Close()
  18. req := httptest.NewRequest(http.MethodPost, "/upload", body)
  19. req.Header.Add("Content-Type", mw.FormDataContentType())
  20. res := httptest.NewRecorder()
  21. // router is of type http.Handler
  22. router.ServeHTTP(res, req)

huangapple
  • 本文由 发表于 2017年5月11日 09:20:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/43904974.html
匿名

发表评论

匿名网友

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

确定