Как выполнить модульное тестирование пары сокет-сервер Java с помощью Spock? - PullRequest
0 голосов
/ 02 апреля 2019

Я пытаюсь написать модульные тесты для сокет-клиента и сервера, используя Спок. Как мне настроить пару сервер / клиент в моих модульных тестах, чтобы она работала?

Я успешно протестировал свой клиентский класс, запустив мой серверный класс вручную вне тестирования, но когда я пытаюсь инициализировать его внутри класса тестирования, кажется, что все тесты либо зависают, либо я получаю отказ подключение.

В данный момент я просто использую слегка модифицированную версию кода EchoServer и EchoClient из Oracle .

Класс клиента:

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();
    }
}

Способ запуска сервера:

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());
    }
}

Испытание на спок:

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"
    }
}

Если я запускаю сервер отдельно вместе с тестами и закомментирую первые две строки внутри «когда:» теста Спока, тест пройдет успешно. Тем не менее, я не могу заставить его работать без зависания теста.

Я должен добавить, что я изучил моделирование, используя это руководство для Заглушки и насмешки в Java с помощью Spock Testing Framework , но у меня нет предыдущего опыта моделирования. поэтому я не увенчался успехом ни с одной из моих попыток использовать его, или знаю, применимо ли вообще использование насмешек в данном конкретном случае.

Ответы [ 2 ]

0 голосов
/ 30 апреля 2019

Решение, предоставленное masooh , работает, но только при условии, что вы запускаете только одного клиента. Если вы хотите запустить несколько тестовых случаев, вам нужно быть осторожным, чтобы вызывать startConnection(..) только один раз, в противном случае тест будет зависать снова. Вот как вы можете решить задачу для интеграционного теста:

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]
  }
}

То, что вам необходимо вручную остановить поток сервера, и что также нет способа закрыть клиентское соединение упорядоченным образом, являются проблемами в коде вашего приложения. Вы должны решить их, предоставив методы закрытия / закрытия для клиента и сервера.

Ситуация становится еще хуже, если вы хотите выполнить юнит-тестирование своего кода:

  • Невозможно внедрить зависимость сокета в клиент, поскольку он сам создает ее, не имея доступа к нему извне и предоставляя макет для тестирования.
  • Если вы хотите покрыть клиентские секции обработки исключений модульными тестами и проверить правильность поведения, вы также заметите, что вызов System.exit(..) из метода внутри вашего клиента - очень плохая идея, потому что он также прервет тест. когда он впервые попадает в этот раздел. Я знаю, что вы скопировали свой код из примера Oracle, но там он использовался в статическом методе main(..), т. Е. Только для случая автономного приложения. Там можно использовать его, но не после того, как вы превратили его в более общий класс клиента.
  • Более общая проблема заключается в том, что даже при комментировании System.exit(..) в вашем клиенте обработка исключений в этом случае будет просто печатать что-то на консоли, но подавлять возникающие исключения, поэтому у пользователя клиентского класса нет простого способа выяснить, что случилось что-то плохое, и разобраться в ситуации. Она останется с неработающим клиентом, потому что по какой-то причине соединение не может быть установлено. Вы все еще можете позвонить по номеру sendMessage(..), но при этом возникнет следующая ошибка.
  • Есть еще вопросы, которые я не буду здесь упоминать, потому что это было бы слишком подробно.

Итак, вы хотите реорганизовать свой код, чтобы сделать его более понятным и более тестируемым. Вот где разработка, управляемая тестами, действительно помогает. Это инструмент проектирования, а не инструмент управления качеством.

Как насчет этого? Я все еще не доволен этим, но он показывает, как легче тестировать код:

Эхо-сервер:

Сервер сейчас

  • обеспечивает прослушивание порта 4444 сервера в отдельном потоке. Больше не нужно запускать дополнительные темы в тесте
  • порождает новый поток для каждого входящего соединения
  • может обрабатывать несколько соединений одновременно (см. Также соответствующий тест интеграции ниже)
  • имеет метод close() и реализует AutoCloseable, т.е. может быть отключен вручную или с помощью try-with-resources.

Я также добавил некоторые записи, в основном для демонстрационных целей, потому что тесты, как правило, ничего не регистрируют, если они пройдут.

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();
  }
}

Эхо-клиент:

Клиент сейчас

  • больше не глотает исключения, но позволяет им возникать и обрабатываться пользователем
  • может получить экземпляр Socket, внедренный через один из его конструкторов, который допускает легкую насмешку и делает класс более тестируемым
  • имеет метод close() и реализует AutoCloseable, то есть может быть отключен вручную или с помощью try-with-resources.

Я также добавил некоторые журналы, в основном для демонстрационных целей, потому что тесты обычно ничего не регистрируют, если они проходят.

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();
  }
}

Интеграционный тест:

Это похоже на ваши собственные решения и решения masooh , но использует обновленные классы клиента и сервера. Вы видите, как легко и клиент, и сервер теперь можно тестировать. На самом деле цель теста - тестировать только клиента, используя сервер только потому, что это интеграционный тест. Но поскольку структура кода обоих классов теперь более линейна, ИТ-отдел фактически создает 100% покрытие линий как для клиента, так и для сервера.

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
  }
}

Юнит-тест:

Я сохранил этот последний, потому что ваш первоначальный вопрос был о насмешке.Итак, теперь я покажу вам, как создать и внедрить фиктивный сокет - или, точнее, заглушку - в ваш клиент через конструктор.Т.е. модульный тест не открывает никаких реальных портов или сокетов, он даже не использует класс сервера.Это действительно юнит-тесты просто клиентского класса.Даже брошенные исключения проверяются.

Кстати, заглушка несколько сложна, действительно ведет себя как эхо-сервер.Я сделал это с помощью потоковых каналов.Конечно, было бы также возможно создать более простой макет / заглушку, который просто возвращает фиксированные результаты.

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
  }
}

PS: Вы можете написать аналогичный модульный тест для сервера, не используя класс клиента или реальныйсокеты, но я оставляю это на ваше усмотрение, чтобы выяснить это, но уже подготовил серверный класс для принятия сокета через инжекцию конструктора.Но вы заметите, что не так просто протестировать метод сервера echo() с помощью mocks, так что, возможно, вы захотите провести там реорганизацию.

0 голосов
/ 04 апреля 2019

Проблема с кодом в том, что echoServer.start() никогда не возвращается, поскольку он остается в цикле while.Поэтому я бы запустил EchoServer в другом потоке.

class EchoClientTest extends Specification {

    @Shared
    Thread echoServerThread

    void setupSpec() {
        echoServerThread = Thread.start {
            EchoServer server = new EchoServer()
            server.start(4444)
        }
    }

    void cleanupSpec() {
        echoServerThread.stop()
    }

    def "Server should echo message from Client"() {
        when:
        EchoClient client = new EchoClient()
        client.startConnection("localhost", 4444)

        then:
        client.sendMessage("echo") == "echo"
    }
}

Имейте в виду, что Thread.stop() устарела.Я просто использовал это, чтобы сделать образец коротким.Смотри https://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html

...