GoでFormFileがあるHandlerのテストを書く

はじめに

Goで、FormFileがあり画像ファイルを読み込むHandlerを書いたがテストでハマってしまったので備忘録として書く。

TL;DR

  • writer.Close()にdeferはつけない

本文

func HogeHandler(w http.ResponseWriter, r *http.Request) {
    if err := r.ParseMultipartForm(32 << 20); err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    fileSrc, fileHeader, err := r.FormFile("upload")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
 
    ...
}

このような、Handlerがあったとする。ParseMultipartFormとかFormFileは詳しくは説明しないがPost formから画像などのファイルを読取るやつである。

詳しく:

note.com

www.irohabook.com

さて、このテストを書く。 実際にhttptestでサーバを起動させ、POSTリクエストを送ることでテストする。

func server() *http.ServeMux {
    mux := http.NewServeMux()

    mux.HandleFunc("/", handler.HogeHandler)

    return mux
}

テストは、画像をos.Openで読み取り、multipartでまとめて送る。 以下のサイトを参考にした。

stackoverflow.com

const IMAGE_PATH= "logo.png"

func TestHoge(t *testing.T) {
    server := httptest.NewServer(handler)

    body := new(bytes.Buffer)
    writer := multipart.NewWriter(body)
    part, err := writer.CreateFormFile("upload", IMAGE_PATH)
    require.NoError(t, err)
    f, err := os.Open(IMAGE_PATH)
    defer f.Close()
    require.NoError(t, err)
    _, err = io.Copy(part, f)
    require.NoError(t, err)
    writer.Close()

    resp, err := http.Post(server.URL+"/", writer.FormDataContentType(), body)
    require.NoError(t, err)
    require.Equal(t, resp.StatusCode, 200)
}

ここで大事なのがwriter.Close()を間違えてdefer writer.Close()と書かないことである。Postする前にcloseしないとEOFエラーがHandlerのParseMultipartFormで発生してしまう。

stackoverflow.com

自分はこれで数時間格闘したのでマジで気をつけてほしい。 実際、write.Close()の実装を見ると、

// Close finishes the multipart message and writes the trailing
// boundary end line to the output.
func (w *Writer) Close() error {
    if w.lastpart != nil {
        if err := w.lastpart.close(); err != nil {
            return err
        }
        w.lastpart = nil
    }
    _, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary)
    return err
}

https://cs.opensource.google/go/go/+/master:src/mime/multipart/writer.go;l=168-179

Close finishes the multipart message and writes the trailing

Closeはマルチパートメッセージを終了し、末尾を書き込みます

確かに書いてある。が、しかし、これは命名で結構勘違いしそうなのでCloseWriteとかEndWriteなどの別の名前にしてほしいと思った。

だってほかのパッケージのCloseはdefer使って最後に実行するものがほとんどなんだもん。