Mocking Sockets in Clojure
Aug 17 2016
In Clojure-land we have access to a vast number of libraries via Java interop. We are free to create Java objects, and call methods on those objects, methods that might mutate the state of those objects.
For the project I have been working on, I needed to use Java’s Socket
API. One problem I faced was figuring out how create a suitable test double for the live socket object. As a relative newcomer to Clojure, it was not immediately apparent to me how I could accomplish this. This post goes over the approach that I took.
Creating a mock1 Socket
First things first. How do we create a fake Socket
object?
We can use proxy
2 to wrap up the Socket
to provide an alternative implementation for some or all of the Socket
class’ methods.
(ns blog-example.mock
(:import java.net.Socket))
(defn socket []
(proxy [Socket] []))
We want simulate that the Socket
begins open, and closes when it receives the close
message. This means that we need to introduce some state.
We can close over the proxy with a let
statement and bind an atom. Here we have bound disconnected?
to an atom set to false
.
(ns blog-example.mock
(:import java.net.Socket))
(defn socket []
(let [disconnected? (atom false)]
(proxy [Socket] [])))
Then we can add implementations for close
and isClosed
to simulate this behaviour without actually creating a connection on the host.
(ns blog-example.mock
(:import java.net.Socket))
(defn socket []
(let [disconnected? (atom false)]
(proxy [Socket] []
(close []
(reset! disconnected? true))
(isClosed []
@disconnected?))))
Now our methods can modify the atom for as long as the proxy is available. Every time a new proxy is created, disconnected?
is false until .close
is called on it.
And here is the function that operates on the socket and its test.
(ns blog-example.socket-io-test
(:require [clojure.test :refer :all]
[blog-example.mock :as mock]))
(defn close [socket]
(doto socket (.close)))
(deftest the-closer
(let [socket (socket "" nil)]
(is (not (.isClosed socket)))
(is (.isClosed (close socket)))))
Reading and writing
This works fine as long as we are only creating sockets and closing them. What about when we want to read from it and write to it?
We can overload the getInputStream
and getOutputStream
and use the ByteArrayInputStream
and ByteArrayOutputStream
instead.
(defn socket [input output]
(let [connected? (atom true)]
(proxy [java.net.Socket] []
(close []
(reset! connected? false))
(isClosed []
(not @connected?))
(getOutputStream []
output)
(getInputStream []
(ByteArrayInputStream.
(.getBytes input))))))
input
is will be a string that the mock will use to create a ByteArrayInputStream
. output
will be a ByteArrayOutputStream
, which we can hold on to to check that our function writes to the socket.
(ns blog-example.socket-io-test
(:require [clojure.test :refer :all]
[blog-example.mock :as mock]))
(defn close [socket]
(doto socket (.close)))
(defn read-from [socket]
(.readLine (io/reader socket)))
(defn write-to [socket string]
(let [writer (io/writer socket)]
(.write writer string)
(.flush writer)
socket))
(deftest the-closer
(let [socket (socket "" nil)]
(is (not (.isClosed socket)))
(is (.isClosed (close socket)))))
(deftest the-reader
(let [socket (socket "Data to read" nil)]
(is (= "Data to read" (read-from socket)))))
(deftest the-writer
(let [output (ByteArrayOutputStream.)
socket (socket "" output)]
(write-to socket "Data to write")
(is (= "Data to write"(.toString output)))))
Wrap up
That’s it. Now we have mocked all of the behavior that we will need from the Socket
. Inside of my project I made sure hide the Socket
objects behind a Clojure abstraction for multiple reasons, one of which being that I wanted to be sure that this type of side-effect code did not seep into other parts of the project.
1. I am using mock to refer a general purpose test double, and not a proper mock.
2. Subclassing can also be done with gen-class
and reify
. Both have there own set of trade-offs.
Recent Articles
- Apprenticeship Retro Oct 14 2016
- What the hell is CORS Oct 13 2016
- Cross-site Tracing Oct 11 2016
- Learning Testing Tools First Oct 7 2016