Merl's Blog

Testing With Dummy Servers

Testing my code is always a crucial part of my development process. Today, I wanted to share how using protocols and dummy servers helped me test my server setup more effectively. I’ll break down the process and the insights I gained along the way.

I implemented a dummy server to test my HTTP server setup, and here it is:

(defprotocol IServer 
             (start [_]))

(deftype FakeServer [started? port handler]
         IServer
         (start [_] (reset! started? true)))

The IServer protocol defines a single method start, and FakeServer is a simple implementation of this protocol. The FakeServer keeps track of whether it has been started and stores the port and handler.

Then I wanted to check if my HTTP server worked as I expected. I created tests to see if the server was initialized correctly and if it has the correct handler.

So I checked that my HTTP server initializes with different ports and that it recognizes its type:

(it "tests MyHTTPServer"
      (let [server-1 (sut/->MyHTTPServer 8080 (TheHandler.))
            server-2 (sut/->MyHTTPServer 9000 (TheHandler.))]
        (should= 8080 (.getPort server-1))
        (should= 9000 (.getPort server-2))
        (should-be-a MyHTTPServer server-1)))

I had no idea I could check for a type, until now. I used should-be-a to check that the servers were instances of MyHTTPServer. Now i can always have confidence that ->MyHTTPServer is fully tested.

Dummy Server

The next step was to ensure that my server starts correctly. I used a dummy server (FakeServer) for this purpose:

(it "begins a dummy server"
      (let [server (atom nil)
            started? (atom false)]
        (with-redefs [sut/->MyHTTPServer (fn [port handler]
                                           (reset! server (->FakeServer started? port handler)))]
          (sut/-main)
          (should= 8080 (.-port @server))
          (should @started?)
          (should-be-a TheHandler (.-handler @server)))))

In this test, I redefined sut/->MyHTTPServer to create an instance of FakeServer instead of the real server. This allowed me to test the server starting process without needing to start an actual HTTP server. I made sure the server was initialized with the correct port and handler, and that it was started correctly.

Code

Here’s the code that I was testing:

(deftype TheHandler []
  IRequestHandler
  (handle [_this request]
    (handler/handle-tictactoe request))
  (canHandle [_this request]
    (= "/tictactoe" (.getPath request))))

(defn ->MyHTTPServer [^Integer port ^IRequestHandler handler]
  (MyHTTPServer. port handler))

(defn start-server []
  (let [port 8080
        tic-tac-toe-handler (TheHandler.)
        server (->MyHTTPServer port tic-tac-toe-handler)]
    (.start server)))

(defn -main [& args]
  (start-server))

The TheHandler type implements the IRequestHandler protocol and handles requests for the “/tictactoe” path. The ->MyHTTPServer function creates a new server instance with the specified port and handler. The start-server function initializes and starts the server.

Using dummy servers made it easier to isolate and test my server code. I have had the ability to test my lower level code, but I have always had fear testing my higher level code.

Which is silly because the higher level the code the more it’s functionality should be tested. And at least for now, I have all of my lines tested.

Testing is an art, and finding the right tools and techniques can make all the difference.

Best,

Merl