英文:
Testing Go http.Request.FormFile?
问题
如何在测试端点时设置Request.FormFile?
部分代码:
func (a *EP) Endpoint(w http.ResponseWriter, r *http.Request) {
...
x, err := strconv.Atoi(r.FormValue("x"))
if err != nil {
a.ren.Text(w, http.StatusInternalServerError, err.Error())
return
}
f, fh, err := r.FormFile("y")
if err != nil {
a.ren.Text(w, http.StatusInternalServerError, err.Error())
return
}
defer f.Close()
...
}
如何使用httptest库生成一个具有可以在FormFile中获取的值的POST请求?
英文:
How do I set the Request.FormFile when trying to test an endpoint?
Partial code:
func (a *EP) Endpoint(w http.ResponseWriter, r *http.Request) {
...
x, err := strconv.Atoi(r.FormValue("x"))
if err != nil {
a.ren.Text(w, http.StatusInternalServerError, err.Error())
return
}
f, fh, err := r.FormFile("y")
if err != nil {
a.ren.Text(w, http.StatusInternalServerError, err.Error())
return
}
defer f.Close()
...
}
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头。
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error)
然后,你可以将这个io.Writer传递给httptest.NewRequest
,它接受一个reader作为参数。
request := httptest.NewRequest("POST", "/", myReader)
为了实现这一点,你可以将FormFile写入一个io.ReaderWriter缓冲区,或者使用io.Pipe。下面是一个完整的示例,使用了管道:
func TestUploadImage(t *testing.T) {
// 设置一个管道以避免缓冲
pr, pw := io.Pipe()
// 这个写入器将把我们传递给它的内容转换为多部分表单数据
// 并将其写入我们的io.Pipe
writer := multipart.NewWriter(pw)
go func() {
defer writer.Close()
// 我们创建了一个名为'fileupload'的表单数据字段
// 它返回另一个写入器来写入实际的文件
part, err := writer.CreateFormFile("fileupload", "someimg.png")
if err != nil {
t.Error(err)
}
// https://yourbasic.org/golang/create-image/
img := createImage()
// Encode()接受一个io.Writer。
// 我们传递了我们之前定义的multipart字段
// 'fileupload',它反过来写入我们的io.Pipe
err = png.Encode(part, img)
if err != nil {
t.Error(err)
}
}()
// 我们从接收数据的管道中读取数据
// 这个管道接收来自multipart写入器的数据
// 而multipart写入器又接收来自png.Encode()的数据。
// 我们有3个链接的写入器!
request := httptest.NewRequest("POST", "/", pr)
request.Header.Add("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
handler := UploadFileHandler()
handler.ServeHTTP(response, request)
t.Log("它应该以HTTP状态码200作为响应")
if response.Code != 200 {
t.Errorf("期望 %s,收到 %d", 200, response.Code)
}
t.Log("它应该在uploads文件夹中创建一个名为'someimg.png'的文件")
if _, err := os.Stat("./uploads/someimg.png"); os.IsNotExist(err) {
t.Error("期望文件'./uploads/someimg.png'存在")
}
}
这个函数利用了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.
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.
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:
func TestUploadImage(t *testing.T) {
// Set up a pipe to avoid buffering
pr, pw := io.Pipe()
// This writer is going to transform
// what we pass to it to multipart form data
// and write it to our io.Pipe
writer := multipart.NewWriter(pw)
go func() {
defer writer.Close()
// We create the form data field 'fileupload'
// which returns another writer to write the actual file
part, err := writer.CreateFormFile("fileupload", "someimg.png")
if err != nil {
t.Error(err)
}
// https://yourbasic.org/golang/create-image/
img := createImage()
// Encode() takes an io.Writer.
// We pass the multipart field
// 'fileupload' that we defined
// earlier which, in turn, writes
// to our io.Pipe
err = png.Encode(part, img)
if err != nil {
t.Error(err)
}
}()
// We read from the pipe which receives data
// from the multipart writer, which, in turn,
// receives data from png.Encode().
// We have 3 chained writers!
request := httptest.NewRequest("POST", "/", pr)
request.Header.Add("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
handler := UploadFileHandler()
handler.ServeHTTP(response, request)
t.Log("It should respond with an HTTP status code of 200")
if response.Code != 200 {
t.Errorf("Expected %s, received %d", 200, response.Code)
}
t.Log("It should create a file named 'someimg.png' in uploads folder")
if _, err := os.Stat("./uploads/someimg.png"); os.IsNotExist(err) {
t.Error("Expected file ./uploads/someimg.png' to exist")
}
}
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
// FormFile返回提供的表单键的第一个文件。
// 如果需要,FormFile会调用ParseMultipartForm和ParseForm。
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
if r.MultipartForm == multipartByReader {
return nil, nil, errors.New("http: multipart handled by MultipartReader")
}
if r.MultipartForm == nil {
err := r.ParseMultipartForm(defaultMaxMemory)
if err != nil {
return nil, nil, err
}
}
if r.MultipartForm != nil && r.MultipartForm.File != nil {
if fhs := r.MultipartForm.File[key]; len(fhs) > 0 {
f, err := fhs[0].Open()
return f, fhs[0], err
}
}
return nil, nil, ErrMissingFile
}
在你的测试中,你应该能够创建一个multipart.Form
的测试实例,并将其赋值给你的请求对象 - https://golang.org/pkg/mime/multipart/#Form
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
当然,这将需要你使用一个真实的文件路径,这在测试的角度来看并不理想。为了解决这个问题,你可以定义一个接口来从请求对象中读取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
// FormFile returns the first file for the provided form key.
1258 // FormFile calls ParseMultipartForm and ParseForm if necessary.
1259 func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
1260 if r.MultipartForm == multipartByReader {
1261 return nil, nil, errors.New("http: multipart handled by MultipartReader")
1262 }
1263 if r.MultipartForm == nil {
1264 err := r.ParseMultipartForm(defaultMaxMemory)
1265 if err != nil {
1266 return nil, nil, err
1267 }
1268 }
1269 if r.MultipartForm != nil && r.MultipartForm.File != nil {
1270 if fhs := r.MultipartForm.File[key]; len(fhs) > 0 {
1271 f, err := fhs[0].Open()
1272 return f, fhs[0], err
1273 }
1274 }
1275 return nil, nil, ErrMissingFile
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
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
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示例中:
func Test_submitFile(t *testing.T) {
path := "testfile.txt"
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("object", path)
assert.NoError(t, err)
sample, err := os.Open(path)
assert.NoError(t, err)
_, err = io.Copy(part, sample)
assert.NoError(t, err)
assert.NoError(t, writer.Close())
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/submit")
if assert.NoError(t, submitFile(c)) {
assert.Equal(t, 200, rec.Code)
assert.Contains(t, rec.Body.String(), path)
fi, err := os.Stat(expectedPath)
if os.IsNotExist(err) {
t.Fatal("Upload file does not exist", expectedPath)
}
assert.Equal(t, wantSize, fi.Size())
}
}
英文:
I combined these and other answers into an Echo example without pipes or goroutines:
func Test_submitFile(t *testing.T) {
path := "testfile.txt"
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("object", path)
assert.NoError(t, err)
sample, err := os.Open(path)
assert.NoError(t, err)
_, err = io.Copy(part, sample)
assert.NoError(t, err)
assert.NoError(t, writer.Close())
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/submit")
if assert.NoError(t, submitFile(c)) {
assert.Equal(t, 200, rec.Code)
assert.Contains(t, rec.Body.String(), path)
fi, err := os.Stat(expectedPath)
if os.IsNotExist(err) {
t.Fatal("Upload file does not exist", expectedPath)
}
assert.Equal(t, wantSize, fi.Size())
}
}
答案4
得分: 6
通过结合之前的答案,以下代码对我起作用:
filePath := "file.jpg"
fieldName := "file"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
file, err := os.Open(filePath)
if err != nil {
t.Fatal(err)
}
w, err := mw.CreateFormFile(fieldName, filePath)
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(w, file); err != nil {
t.Fatal(err)
}
// 在发送请求之前关闭写入器
mw.Close()
req := httptest.NewRequest(http.MethodPost, "/upload", body)
req.Header.Add("Content-Type", mw.FormDataContentType())
res := httptest.NewRecorder()
// router 是 http.Handler 类型
router.ServeHTTP(res, req)
请注意,这只是代码的翻译部分,不包括任何其他内容。
英文:
By combining the previous answers, this worked for me:
filePath := "file.jpg"
fieldName := "file"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
file, err := os.Open(filePath)
if err != nil {
t.Fatal(err)
}
w, err := mw.CreateFormFile(fieldName, filePath)
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(w, file); err != nil {
t.Fatal(err)
}
// close the writer before making the request
mw.Close()
req := httptest.NewRequest(http.MethodPost, "/upload", body)
req.Header.Add("Content-Type", mw.FormDataContentType())
res := httptest.NewRecorder()
// router is of type http.Handler
router.ServeHTTP(res, req)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论