Use an in-memory listener for unit tests

Golang's builtin net/http/httptest package allows you to test code that implements an HTTP server meeting the http.Handler interface. It does this by listening for connections on the loopback interface on the first available socket. This works fine, but presumes that you can get a listening socket on the loopback interface of your test machine. This is generally the case, but we can do better.

Making an in-memory listener

The net package now has the net.Pipe function that gives two connected sockets in memory. To test a listening socket server in memory this needs to be adapted to function as a net.Listener. That interface is as follows

type Listener interface {
        // Accept waits for and returns the next connection to the listener.
        Accept() (c Conn, err error)

        // Close closes the listener.
        // Any blocked Accept operations will be unblocked and return errors.
        Close() error

        // Addr returns the listener's network address.
        Addr() Addr
}

So, we need Accept() to return a connection to a client that is trying to connect. The other two functions need to exist, but their purpose is less important for unit testing.

The Accept() function is blocking until a client connects or an error occurs. To create this sort of behavior, we can use a channel.

func (ml *MemoryListener) Accept() (net.Conn,error) {
    select {
        case newConnection := <- ml.connections:
            return newConnection, nil
        case <- ml.state:  
            return nil,errors.New("Listener closed")
    }
}   

By using a channel, the listening server blocks until a new connection is passed over ml.connections. When ml.state is closed Accept() immediately returns an error indicating the listener has been closed. In order to get connections to the server blocking in Accept() we'll need another function to open new connections.

func (ml *MemoryListener) Dial(network, addr string) (net.Conn,error) {
    //Check for being closed
    select {
        case <- ml.state:
            return nil, errors.New("Listener closed")
        default:
    }
    //Create the in memory transport
    serverSide, clientSide := net.Pipe()
    //Pass half to the server
    ml.connections <- serverSide
    //Return the other half to the client
    return clientSide, nil
}

This allows a connection to be established to the server using the same MemoryListener instance. For testing a simple socket server, this is sufficient. The normal net.Listener used by the server is replaced with an instance of MemoryListener. For the client, you'll need to be able to explicitly specify an already existing net.Conn to use or be able to set a function the client uses to open a new connection. Here is an example in github.com/garyburd/redigo of what I mean.

Testing an HTTP servers

The goal of this is test HTTP servers using an in memory listener. There is no easy way to marry this to existing test framework net/http/httptest unfortunately. It is completely possible to use this with package net/http. All we need to do is construct an instance of http.Server that uses an instance of MemoryListener. This can be wrapped up as follows

func NewInMemoryServer(h http.Handler) *MemoryServer {
    retval := &MemoryServer{}
    retval.Listener = NewMemoryListener()
    retval.Server = &http.Server{}
    retval.Handler = h
    go retval.Serve(retval.Listener)
    return retval
}

In order to make a request to the HTTP server, you'll need to use an instance of http.Client that opens a connection to the MemoryListener. The http.Client type relies on an implementation of http.RoundTripper to select the appropriate network transport for connecting to the server. The type http.Transport typically fills this role. The implementation of (*MemoryListener).Dial takes two string arguments which are ignored. This allows the (*MemoryListener).Dial function to be used as the value of the member Dial of http.Transport. This results in all requests regardless of the domain in the URL going to the server under test.

All of the behavior required to instantiate an http.Client that makes HTTP requests to the server under test can be wrapped up into a single function.

func (ms *MemoryServer) NewTransport() *http.Transport {
    transport := &http.Transport{}
    transport.Dial = ms.Listener.Dial
    return transport
}

func (ms *MemoryServer) NewClient() *http.Client {
    client := &http.Client{}
    client.Transport = ms.NewTransport()
    return client
}

Requests can be made to the server under test by using ms.NewClient().Get("http://server.test/foo"). This creates a complete environment to test your HTTP server without relying on the network stack of the machine running the tests.

Putting it all together

I've put all of this together in a complete library github.com/hydrogen18/memlistener. There is an example of how to use it for the purposes of testing an http.Handler in the package github.com/hydrogen18/memlistener/example.


© Eric Urban 2014