Testing cohttp_eio request handlers

Hey,

I wrote a webservice using cohttp_eio and then wanted to test it, ideally without having to start a server listening on a network interface.

My rough idea was to craft the inputs for the request handler (HTTP verb, resource, headers, body) and then call the handler under test and finally inspect the output. Sounds easy… :slight_smile:

FYI, a request handler in cohttp_eio is just a function with three arguments conn, request and body which returns a response.

BUT: The conn and response caused me headaches as they are private types. While I was able to just drop the conn argument from my handlers as they are not used, the response was still there and is of type:

type response = writer -> unit

with an abstract type writer

I wasn’t able to figure out how to create a writer, so I discarded my initial idea and went with starting a Cohttp_eio.Server and using Cohttp_eio.Client to send real requests. Thanks to Eio’s deterministic “scheduling” I was able to do:

let stop, stop_signal = Eio.Promise.create () in
Eio.Fiber.both
      (fun () ->
        start_server ~stop ())
      (fun () ->
        test_server ();
        Eio.Promise.resolve stop_signal ())

No “ugly” polling required to wait until the server is up and running until I can start sending requests. Not sure if this approach is 100% bullet proof, but it seems to work; otherwise another Promise to signal “started” would work.

For testing request handlers, I think it would be nice to be able to “mock” a Cohttp_eio connection and writer. I am just not sure if it’s worth it.

Probably a better approach would be for me to completely hide the use of Cohttp_eio from withing my handler’s code and use my own representation of a response (and body). So instead of Cohttp_eio.Server.respond_string I’d have to return my own representation. And the body argument would become a simple function fun () -> Eio.Flow.read_all body.

I am happy to hear your view and how you would test request handlers (if you test them) :slight_smile:

Seems reasonable to me. Might as well test it with the real cohttp logic!

Server.run takes a listening_socket. So I’d create that first (outside of the Fiber.both). Then it doesn’t matter if the server yields while starting up.

Thanks! I initially created the listening_socket within the first Fiber and it seemed to work fine, but now moved it out, just to be safe.

I run into a github ci problem with that code, but that turned out to be unrelated. When run from a github action you need to use “127.0.0.1” instead of “localhost” in the HTTP client.