goreverse-proxymartini

How to test reverse proxy with martini in go


I'm writing test code for martini app working as a reverse proxy in go, and want to test it using httptest.ResponseRecorder, but I got an error the following.

[martini] PANIC: interface conversion: *httptest.ResponseRecorder is not http.CloseNotifier: missing method CloseNotify

httptest.ResponseRecorder has no method CloseNotify()

How should I test it?

package main

import (
        "github.com/go-martini/martini"
        "github.com/stretchr/testify/assert"
        "net/http"
        "net/http/httptest"
        "net/http/httputil"
        "net/url"
        "testing"
)

func TestReverseProxy(t *testing.T) {
        // Mock backend
        backendResponse := "I am the backend"
        backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                w.Write([]byte(backendResponse))
        }))
        defer backend.Close()
        backendURL, _ := url.Parse(backend.URL)

        // Frontend
        m := martini.Classic()
        m.Get("/", func(w http.ResponseWriter, r *http.Request) {
                proxy := httputil.NewSingleHostReverseProxy(backendURL)
                proxy.ServeHTTP(w, r)
        })

        // Testing
        req, _ := http.NewRequest("GET", "/", nil)
        res := httptest.NewRecorder()
        m.ServeHTTP(res, req)

        assert.Equal(t, 200, res.Code, "should be equal")
}

Solution

  • First, please note that the martini framework is no longer maintained as said in their README.

    Then, about your issue, it's because Martini does something that looks pretty bad to me: it takes an http.ResponseWriter and assumes it is also an http.CloseNotifier, while there is absolutely no guarantee of this. They should take a custom interface wrapping both of them, something like that:

    type ResponseWriterCloseNotifier interface {
        http.ResponseWriter
        http.CloseNotifier
    }
    

    You can see in their source code that they had the same issue for their own tests, and used some workaround: https://github.com/go-martini/martini/commit/063dfcd8b0f64f4e2c97f0bc27fa422969baa23b#L13

    Here is some working code made with it:

    package main
    
    import (
        "net/http"
        "net/http/httptest"
        "net/http/httputil"
        "net/url"
        "testing"
    
        "github.com/go-martini/martini"
        "github.com/stretchr/testify/assert"
    )
    
    type closeNotifyingRecorder struct {
        *httptest.ResponseRecorder
        closed chan bool
    }
    
    func newCloseNotifyingRecorder() *closeNotifyingRecorder {
        return &closeNotifyingRecorder{
            httptest.NewRecorder(),
            make(chan bool, 1),
        }
    }
    
    func (c *closeNotifyingRecorder) close() {
        c.closed <- true
    }
    
    func (c *closeNotifyingRecorder) CloseNotify() <-chan bool {
        return c.closed
    }
    
    func TestReverseProxy(t *testing.T) {
        // Mock backend
        backendResponse := "I am the backend"
        backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte(backendResponse))
        }))
        defer backend.Close()
        backendURL, _ := url.Parse(backend.URL)
    
        // Frontend
        m := martini.Classic()
        m.Get("/", func(w http.ResponseWriter, r *http.Request) {
            proxy := httputil.NewSingleHostReverseProxy(backendURL)
            proxy.ServeHTTP(w, r)
        })
    
        // Testing
        req, _ := http.NewRequest("GET", "/", nil)
        res := newCloseNotifyingRecorder()
        m.ServeHTTP(res, req)
    
        assert.Equal(t, 200, res.Code, "should be equal")
    }