当前位置: 首页 > 知识库问答 >
问题:

如何使用Spock对Java套接字服务器/客户机对进行单元测试?

濮书
2023-03-14

我试图用Spock为套接字客户端和服务器编写单元测试。我应该如何在单元测试中设置服务器/客户机对,以便它能够工作?

通过在测试之外手动运行服务器类,我成功地测试了我的客户端类,但是当我试图在测试类内部初始化它时,所有的测试似乎要么挂起,要么得到一个拒绝的连接。

目前,我使用的只是Oracle的EchoServer和EchoClient代码的稍微修改版本。

客户端类:

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

共有1个答案

单修德
2023-03-14

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

您需要手动停止服务器线程,并且无法以有序的方式关闭客户端连接,这些都是应用程序代码中的问题。您应该通过为客户端和服务器提供close/shutdown方法来解决这些问题。

如果要对代码进行单元测试,情况会变得更糟:

    null

因此,您希望重构代码,使其更具可维护性和可测试性。这就是测试驱动开发真正有帮助的地方。它是一种设计工具,而主要不是质量管理工具。

这样怎么样?我仍然对它不满意,但它显示了测试代码是如何变得更容易的:

Echo服务器:

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

集成测试:

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

附注:您可以为服务器编写一个类似的单元测试,而不是使用客户端类或真正的套接字,但我让您来解决这个问题,但您已经准备好了服务器类也可以通过构造函数注入接受套接字。但是您会注意到,使用mocks测试服务器的echo()方法并不那么容易,因此可能需要在那里进行更多的重构。

 类似资料:
  • 问题内容: 我目前正在编写Java客户端服务器应用程序。所以我想实现两个库,一个用于客户端,一个用于服务器。客户端服务器通信具有非常严格的协议,我不打算使用JUnit进行测试。 作为构建工具,我使用Maven和Husdon Server进行持续集成。 实际上,我对如何测试这些客户端/服务器库没有什么好主意。 我得到以下方法: 只需编写一个虚拟客户端来测试服务器,然后编写一个虚拟服务器来测试客户端。

  • 问题内容: 我正在尝试创建一个简单的单元测试来测试我的表演功能。 我收到以下错误: 看来这不是控制器的范围吗? 这是我的控制器: 这是我的控制器单元测试: 问题答案: 为什么不简单地使用spyOn函数? 希望这可以帮助!

  • 我试图用java实现一个客户端服务器,在这里我读取客户端中的输入并在服务器中执行UperCase,然后返回客户端并打印UperCase。我使用ObjectOutputStream和ObjectInputStream进行读写,但是当我在客户机中键入一个msg时,程序会显示以下错误: Digite uma msg casa java.io.eofexception位于java.io.datainput

  • 我在go中对gRPC服务进行单元测试时遇到困难。 我看了一下测试gRPC服务,但它对我不起作用,不确定我做错了什么。 Add方法的gRPC服务实现: Add方法的单元测试: 项目目录结构 抛出一个错误,称为。 我对非常陌生,找不到解决方法。

  • 问题内容: 我试图用没有gui的服务器连接带有gui的客户端。连接已完成,但我看不到这两个应用程序之间的任何消息。(我应该在客户端中找到SERVER HERE,在服务器中找到CLIENT HERE) 客户端连接代码: (输入和输出在此客户端类扩展到的GUI类中定义。定义为“受保护的BufferedReader输入;受保护的PrintWriter输出;”) 另外,服务器代码: 连接似乎还可以,所以我

  • 我组装了一个Java套接字服务器和客户端,可以互相发送消息,我想使用JavaScript作为客户端,但是。。。下面是在托管Java服务器并加载JavaScript客户端时发生的情况。 JavaScript: 这将在chrome控制台中打印: 到“ws://127.0.0.1:9005/”的WebSocket连接失败:WebSocket握手期间出错:无效状态行 WebSocket错误[对象事件] 我