Search code examples
javaunit-testingsocketsspock

How to unit-test Java socket server/client pair using Spock?


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.


Solution

  • 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:

    • There is no way to inject the socket dependency into the client because it creates it by itself with no way to access it from outside and provide a mock for testing.
    • If you want to cover the client's exception handling sections with unit tests and check for the correct behaviour, you will also notice that calling 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.
    • A more general issue is that even when commenting out 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.
    • There are more issues which I won't mention here because it would be too much detail.

    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

    • takes care of listening on the server port 4444 in a separate thread. No more need to start extra threads in a test
    • spawns another new thread for each incoming connection
    • can handle multiple connections at the same time (see also corresponding integration test below)
    • has a 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

    • no longer swallows exceptions but lets them happen and be handled by the user
    • can get a Socket instance injected via one of its constructors, which permits for easy mocking and makes the class more testable
    • has a 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.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.