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

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

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)

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:

确定