Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing Go http.Request.FormFile?

Tags:

go

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?

like image 658
Nathan Hyland Avatar asked May 11 '17 01:05

Nathan Hyland


Video Answer


2 Answers

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.

like image 174
Federico Avatar answered Nov 15 '22 06:11

Federico


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

like image 20
Eamonn McEvoy Avatar answered Nov 15 '22 07:11

Eamonn McEvoy