I'm trying to write unit tests for a socket client and server using Spock. How should I be setting up the server/client pair in my unit tests so that it will work?
I've had success testing my client class by running my server class manually outside of the testing, but when I try to initialise it inside of the testing class, all of the tests seem to either hang, or I get a refused connection.
At the moment I'm just using a slightly modified version of the EchoServer and EchoClient code from Oracle.
Client class:
public class EchoClient {
private Socket echoSocket;
private PrintWriter out;
private BufferedReader in;
public void startConnection(String hostName, int portNumber) throws IOException {
try {
echoSocket = new Socket(hostName, portNumber);
out = new PrintWriter(echoSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream()));
} catch (UnknownHostException e) {
System.err.printf("Don't know about host %s%n", hostName);
System.exit(1);
} catch (IOException e) {
System.err.printf("Couldn't get I/O for the connection to %s%n", hostName);
System.exit(1);
}
}
public String sendMessage(String msg) throws IOException {
out.println(msg);
return in.readLine();
}
}
Server start method:
public void start(int portNumber) throws IOException {
try (
ServerSocket serverSocket =
new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
out.println(inputLine);
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port "
+ portNumber + " or listening for a connection");
System.out.println(e.getMessage());
}
}
Spock test:
class EchoClientTest extends Specification {
def "Server should echo message from Client"() {
when:
EchoServer server = new EchoServer()
server.start(4444)
EchoClient client = new EchoClient()
client.startConnection("localhost", 4444)
then:
client.sendMessage("echo") == "echo"
}
}
If I run the server separately alongside the tests, and comment out the first two lines inside the 'when:' of the Spock test, the test will run successfully. However, I cannot get it to run without hanging on the test otherwise.
I should add that I've looked into mocking by using this guide for Stubbing and Mocking in Java with the Spock Testing Framework, but I've got no previous experience mocking so I've not been successful with any of my attempts to use it, or know if it is applicable to use mocking at all in this particular case.
The solution provided by masooh works, but only as long as you only start one client. If you want to run multiple test cases, you need to be careful to only call startConnection(..)
once, otherwise the test will hang again. This is how you can solve the problem for the integration test:
package de.scrum_master.stackoverflow.q55475971
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientIT extends Specification {
static final int SERVER_PORT = 4444
static Thread echoServerThread
static EchoClient echoClient
void setupSpec() {
echoServerThread = Thread.start {
new EchoServer().start(SERVER_PORT)
}
echoClient = new EchoClient()
echoClient.startConnection("localhost", SERVER_PORT)
}
void cleanupSpec() {
echoServerThread?.stop()
}
@Unroll
def "server echoes client message '#message'"() {
expect:
echoClient.sendMessage(message) == message.toString()
where:
message << ["echo", "Hello world!", null]
}
}
That you need to manually stop the server thread and that there is also no way to close the client connection in an orderly manner are problems in your application code. You should address them by providing close/shutdown methods for both client and server.
The situation gets even worse if you want to unit-test your code:
System.exit(..)
from a method inside your client is a really bad idea because it will also interrupt the test when it first hits that section. I know you copied your code from the Oracle example, but there it was used in the static main(..)
method, i.e. only for the case of a stand-alone application. There it is okay to use it, but no more after your refactoring it into a more general client class.System.exit(..)
in your client, the exception handling in this case would just print something on the console but suppress the occurring exceptions, so the user of the client class has no easy way to find out that something bad happened and to handle the situation. She would be left with a non-working client because for some reason the connection could not be established. You could still call sendMessage(..)
but a subsequent error would occur.So you want to refactor your code to make it more maintainable and also more testable. This is where test-driven development really helps. It is a design tool, not mainly a quality management tool.
How about this? I am still not happy with it, but it shows how it gets easier to test the code:
Echo server:
The server now
close()
method and implements AutoCloseable
, i.e. can be shut down manually or via try-with-resources.I also added some logging, mainly for demonstration purposes because tests usually don't log anything if they pass.
package de.scrum_master.stackoverflow.q55475971;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class EchoServer implements AutoCloseable {
private ServerSocket serverSocket;
public EchoServer(int portNumber) throws IOException {
this(new ServerSocket(portNumber));
}
public EchoServer(ServerSocket serverSocket) throws IOException {
this.serverSocket = serverSocket;
listen();
System.out.printf("%-25s - Echo server started%n", Thread.currentThread());
}
private void listen() {
Runnable listenLoop = () -> {
System.out.printf("%-25s - Starting echo server listening loop%n", Thread.currentThread());
while (true) {
try {
echo(serverSocket.accept());
} catch (IOException e) {
System.out.printf("%-25s - Stopping echo server listening loop%n", Thread.currentThread());
break;
}
}
};
new Thread(listenLoop).start();
}
private void echo(Socket clientSocket) {
Runnable echoLoop = () -> {
System.out.printf("%-25s - Starting echo server echoing loop%n", Thread.currentThread());
try (
Socket socket = clientSocket;
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
out.println(inputLine);
System.out.printf("%-25s - Echoing back message: %s%n", Thread.currentThread(), inputLine);
}
System.out.printf("%-25s - Stopping echo server echoing loop%n", Thread.currentThread());
} catch (IOException e) {
e.printStackTrace();
}
};
new Thread(echoLoop).start();
}
@Override
public void close() throws Exception {
System.out.printf("%-25s - Shutting down echo server%n", Thread.currentThread());
if (serverSocket != null) serverSocket.close();
}
}
Echo client:
The client now
Socket
instance injected via one of its constructors, which permits for easy mocking and makes the class more testableclose()
method and implements AutoCloseable
, i.e. can be shut down manually or via try-with-resources.I also added some logging, mainly for demonstration purposes because tests usually don't log anything if they pass.
package de.scrum_master.stackoverflow.q55475971;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class EchoClient implements AutoCloseable {
private Socket echoSocket;
private PrintWriter out;
private BufferedReader in;
public EchoClient(String hostName, int portNumber) throws IOException {
this(new Socket(hostName, portNumber));
}
public EchoClient(Socket echoSocket) throws IOException {
this.echoSocket = echoSocket;
out = new PrintWriter(echoSocket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(echoSocket.getInputStream()));
System.out.printf("%-25s - Echo client started%n", Thread.currentThread());
}
public String sendMessage(String msg) throws IOException {
System.out.printf("%-25s - Sending message: %s%n", Thread.currentThread(), msg);
out.println(msg);
return in.readLine();
}
@Override
public void close() throws Exception {
System.out.printf("%-25s - Shutting down echo client%n", Thread.currentThread());
if (out != null) out.close();
if (in != null) in.close();
if (echoSocket != null) echoSocket.close();
}
}
Integration test:
This is similar to your own and masooh's solutions, but uses the updated client and server classes. You see how easy both client and server are testable now. Actually the test's purpose is to only test the client, using the server only because it is an integration test. But because both classes' code structure is now more linear, the IT actually creates 100% line coverage for both client and server.
package de.scrum_master.stackoverflow.q55475971
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientIT extends Specification {
static final int SERVER_PORT = 4444
@Shared
EchoClient echoClient
@Shared
EchoServer echoServer
void setupSpec() {
echoServer = new EchoServer(SERVER_PORT)
echoClient = new EchoClient("localhost", SERVER_PORT)
}
void cleanupSpec() {
echoClient?.close()
echoServer?.close()
}
@Unroll
def "server echoes client message '#message'"() {
expect:
echoClient.sendMessage(message) == message.toString()
where:
message << ["echo", "Hello world!", null]
}
def "multiple echo clients"() {
given:
def echoClients = [
new EchoClient("localhost", SERVER_PORT),
new EchoClient("localhost", SERVER_PORT),
new EchoClient("localhost", SERVER_PORT)
]
expect:
echoClients.each {
assert it.sendMessage("foo") == "foo"
}
echoClients.each {
assert it.sendMessage("bar") == "bar"
}
cleanup:
echoClients.each { it.close() }
}
@Unroll
def "client creation fails with #exceptionType.simpleName when using illegal #connectionInfo"() {
when:
new EchoClient(hostName, portNumber)
then:
thrown exceptionType
where:
connectionInfo | hostName | portNumber | exceptionType
"host name" | "does.not.exist" | SERVER_PORT | UnknownHostException
"port number" | "localhost" | SERVER_PORT + 1 | IOException
}
}
Unit test:
I saved this one for last because your original question was about mocking. So now I am showing you how to create and inject a mock socket - or a stub, more exactly - into your client via constructor. I.e. the unit test does not open any real ports or sockets, it does not even use the server class. It really unit-tests just the client class. Even the thrown exceptions are tested.
BTW, the stub is somewhat elaborate, really behaving like the echo server. I did this via piped streams. Of course it would also have been possible to create a simpler mock/stub which just returns fixed results.
package de.scrum_master.stackoverflow.q55475971
import spock.lang.Specification
import spock.lang.Unroll
class EchoClientTest extends Specification {
@Unroll
def "server echoes client message '#message'"() {
given:
def outputStream = new PipedOutputStream()
def inputStream = new PipedInputStream(outputStream)
def echoClient = new EchoClient(
Stub(Socket) {
getOutputStream() >> outputStream
getInputStream() >> inputStream
}
)
expect:
echoClient.sendMessage(message) == message.toString()
cleanup:
echoClient.close()
where:
message << ["echo", "Hello world!", null]
}
def "client creation fails for unreadable socket streams"() {
when:
new EchoClient(
Stub(Socket) {
getOutputStream() >> { throw new IOException("cannot read output stream") }
getInputStream() >> { throw new IOException("cannot read input stream") }
}
)
then:
thrown IOException
}
def "client creation fails for unknown host name"() {
when:
new EchoClient("does.not.exist", 4444)
then:
thrown IOException
}
}
P.S.: You could write a similar unit test for the server, not using the client class or real sockets, but I leave it up to you to figure that out but already prepared the server class to also accept a socket via constructor injection. But you will notice that it is not quite as easy to test the server's echo()
method with mocks, so maybe you want to refactor more there.