Решение, предоставленное 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, так что, возможно, вы захотите провести там реорганизацию.