Note: this was originally written on Dec 3, 2006
When i started working on IrcLeech, i didn't have testability in mind. I wasn't even planning on writing tests for it because i figured it'd be too much of a hassle to write decent tests for code that's mostly dealing with communicating over a network connection. Some pieces of code are hard to test, but they don't have to be. I'm gonna walk you through a piece of code of my IrcLeech project to show you how easy it can actually be.
At first, i had a Download class which would connect to an IRC bot to download a file. To connect to the bot, i needed to use a Socket object. I made the mistake of directly using the Socket. It made my code hard to test because the Socket dependency could make my tests fail due to network problems for instance. I had the same problem in my ServerConnection class, which handles the communication between the application and an IRC server. It also used a Socket object directly. The first step was to create my own Socket interface. I basically just looked at all the methods of the real Socket class i used and defined them in my own Socket interface:
public interface Socket
{
void connect(String host, int port) throws UnknownHostException, IOException;
void connect(InetAddress address, int port) throws IOException;
void close() throws IOException;
boolean isConnected();
InetAddress getLocalAddress();
InputStream getInputStream() throws IOException;
OutputStream getOutputStream() throws IOException;
}
Then i wrote a ConcreteSocket class which just wrapped the real Socket through this interface:
public class ConcreteSocket implements Socket
{
private java.net.Socket _javaSocket;
public void connect(String host, int port) throws UnknownHostException, IOException
{
_javaSocket = new java.net.Socket(host, port);
}
public void connect(InetAddress address, int port) throws IOException
{
_javaSocket = new java.net.Socket(address, port);
}
public void close() throws IOException
{
_javaSocket.close();
}
public boolean isConnected()
{
return _javaSocket.isConnected();
}
public InetAddress getLocalAddress()
{
return _javaSocket.getLocalAddress();
}
public InputStream getInputStream() throws IOException
{
return _javaSocket.getInputStream();
}
public OutputStream getOutputStream() throws IOException
{
return _javaSocket.getOutputStream();
}
}
Then, i needed a way to instantiate an object which implemented my Socket interface without having any knowledge of the ConcreteSocket class. So i created a SocketFactory:
public abstract class SocketFactory
{
public abstract Socket createSocket();
}
And of course, a ConcreteSocketFactory:
public class ConcreteSocketFactory extends SocketFactory
{
public Socket createSocket()
{
return new ConcreteSocket();
}
}
Now i have everything i need to get rid of the direct dependency on Java's Socket class in my Download class. Instead of calling the constructor of the Java Socket, i can ask a SocketFactory for a Socket instead, without actually knowing what kind of Socket i'm talking to. This way, i can make sure my Download class uses a fake socket when it's used in unit tests. First, we need a way to pass the SocketFactory to a Download object, so we add a setSocketFactory method to the Download class:
private SocketFactory _socketFactory;public void setSocketFactory(SocketFactory socketFactory) { _socketFactory = socketFactory; }
When the application is running, a Download object will always receive an instance of ConcreteSocketFactory. But when i'm testing, the Download object will use another SocketFactory, one that creates fake Sockets.
This is how i originally created the real Socket:
_socket = new java.net.Socket(_address, _port);
but now, it can be replaced with:
_socket = _socketFactory.createSocket(); _socket.connect(_address, _port);
Now it's time to actually make use of these changes. I wrote a unit test which simulates a succesful download. For that, i need a fake Socket, and a SocketFactory that instantiates these fake Sockets:
class TestSocketFactory extends SocketFactory
{
public Socket createSocket()
{
return new TestSocket();
}
}
class TestSocket implements Socket
{
public static int FILE_SIZE = 20000;
public static String FILE_CONTENT = Utils.createRandomString(FILE_SIZE);
private ByteArrayInputStream _inputStream;
public void connect(InetAddress address, int port) throws IOException
{
_inputStream = new ByteArrayInputStream(FILE_CONTENT.getBytes());
}
public void close() throws IOException
{
_inputStream = null;
}
public InputStream getInputStream() throws IOException
{
return _inputStream;
}
public boolean isConnected()
{
throw new UnsupportedOperationException("Not needed for this test.");
}
public InetAddress getLocalAddress()
{
throw new UnsupportedOperationException("Not needed for this test.");
}
public void connect(String host, int port) throws UnknownHostException,
IOException
{
throw new UnsupportedOperationException("Not needed for this test.");
}
public OutputStream getOutputStream() throws IOException
{
throw new UnsupportedOperationException("Not needed for this test.");
}
}
As you can see, writing a fake socket is pretty easy. It just sets up an InputStream object with some random data. The Download object will call the getInputStream() method and will then get the data out of the stream and write it to a file. Since we're simulating a succesful download, we just need to set up a Download object with a reference to the TestSocketFactory, tell it to download the 'file' and then compare the 'downloaded' file with the content of the original stream. If the data in both is identical, the test passes.
Here's the code to run the test:
public void testSuccesfulDownload()
{
Download download = new Download(_fileName, null, null, 0, TestSocket.FILE_SIZE);
download.setSocketFactory(new TestSocketFactory());
download.connect();
while (!download.isFinished())
{
download.receiveData();
}
download.disconnect();
assertTrue(isDownloadedFileCorrect());
}
private boolean isDownloadedFileCorrect()
{
try
{
FileReader reader = new FileReader(_fileName);
char[] chars = new char[TestSocket.FILE_SIZE];
reader.read(chars);
reader.close();
return (new String(chars).equals(TestSocket.FILE_CONTENT));
}
catch (Exception ex)
{
ex.printStackTrace();
}
return false;
}
As you can see, none of this was particularly hard to do. The hard part is coming up with good tests. This was just one testcase. To thoroughly test the Download class i need to add some more tests. One that verifies whether the Download class properly connects to a bot. One that verifies if it properly disconnects from the bot. One that verifies if an interrupted download is dealt with accordingly, etc... I hope this walkthrough was useful to you. I would like to note that i do know i can avoid the Factory classes by using an Inversion of Control container, but for the sake of clarity i chose to go with the simple Factories instead. Also, in most cases you can just inject the dependency in the class you're testing instead of injecting a factory which can create the dependency. It usually just depends on who the rightful owner of the dependency is.
If anyone knows any better ways to increase testability, i'd be happy to hear about it. This was only my first attempt ![]()