模拟与Mockito的服务器 - 客户端连接

kar*_*bar 7 java sockets multithreading unit-testing mockito

介绍

我试图通过从一个线程向另一个线程发送字符串来测试套接字连接,其中服务器和客户端套接字使用Mockito v1.9.5进行模拟.

这是我正在尝试运行的测试:

@Test
public void testConnection() {        
    //associate a mock server socket with the TCP Connection
    TcpSocketConnection connection = new TcpSocketConnection(mockServerSocket);
    try {
        //begin thread which listens for clients, then sends "Hello, world" to a connected
        //client.
        connection.listen();
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(mockTestClientSocket.getInputStream(), DEFAULT_CHARSET)
        );

        long startTime = System.nanoTime();
        for (long interval = 0;
                interval < TIMEOUT_TIME;
                interval = System.nanoTime() - startTime) {
            if (reader.ready()) {
                String receivedMessage = reader.readLine();
                assertEquals("Hello, world!", receivedMessage);
                mockTestClientSocket.close();
                connection.closeSocket();
                return;
            }
        }
        mockTestClientSocket.close();
        connection.closeSocket();
        fail("Failed to receive message.");
    } catch (IOException e) {
        fail(e.getMessage());
    }
}
Run Code Online (Sandbox Code Playgroud)

测试运行直到TIMEOUT_TIME然后失败的断言是"Failed to receive message." 我在这里指定模拟对象的行为:

@Before
public void setup() {
    mockServerSocket = mock(ServerSocket.class);
    try {
        when(mockServerSocket.accept()).thenReturn(mockTestClientSocket);
    } catch (IOException e) {
        fail(e.getMessage());
    }

    mockTestClientSocket = mock(Socket.class);
    try {
        PipedOutputStream oStream = new PipedOutputStream();
        when(mockTestClientSocket.getOutputStream()).thenReturn(oStream);

        PipedInputStream iStream = new PipedInputStream(oStream);
        when(mockTestClientSocket.getInputStream()).thenReturn(iStream);

        when(mockTestClientSocket.isClosed()).thenReturn(false);
    } catch (IOException e) {
        fail(e.getMessage());
    }
}
Run Code Online (Sandbox Code Playgroud)

我正在尝试测试的部分内容是run()在内部类中的以下内容connection.listen():

class InnerListenerClass implements Runnable {
    @Override
    public void run() {
        try {
            clientSocket = socket.accept();
            writer = new OutputStreamWriter(
               clientSocket.getOutputStream(), DEFAULT_CHARSETNAME);
            out = new PrintWriter(writer, true);
            while (!clientSocket.isClosed()) {
                out.println("Hello, world!");
                Thread.sleep(MILLIS_BETWEEN_MESSAGES);
            }
        } catch (InterruptedException | IOException e) {
            LOG.debug(e.getMessage());
        }
    }

    public InnerListenerClass(final ServerSocket socket) {
        this.socket = socket;
    } 
}
Run Code Online (Sandbox Code Playgroud)

这是以下部分内容TcpSocketConnection.java:

class TcpSocketConnection() {
    public TcpSocketConnection(final ServerSocket serverSocket) {
        checkNotNull(serverSocket);
        this.serverSocket = serverSocket;
    }
    ...
    public final void listen() throws IOException {
        listenerThread = new Thread(new InnerListenerClass(serverSocket));
        listenerThread.start();
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

它是如何在我脑海中起作用的

这部分是可以忽略的; 我将尝试逐步完成测试过程,为此问题添加一些额外的上下文.从头开始testConnection():

TcpSocketConnection connection = new TcpSocketConnection(mockServerSocket);
Run Code Online (Sandbox Code Playgroud)

这将与与之关联的模拟ServerSocket创建连接.这将创建一个使用以下行开始的线程:

clientSocket = socket.accept();
Run Code Online (Sandbox Code Playgroud)

由于socket上面是一个引用mockServerSocket,Mockito知道返回对mockTestClientSocket由于这一行调用的模拟Socket的引用:

when(mockServerSocket.accept()).thenReturn(mockTestClientSocket);
Run Code Online (Sandbox Code Playgroud)

接下来是下面的一行:注意:我相信这是我理解和现实不同的地方,因为我相信基于调试这个线程在创建这个OutputStreamWriter对象时会挂起.我还没弄清楚原因.

writer = new OutputStreamWriter(clientSocket.getOutputStream(), DEFAULT_CHARSETNAME);
Run Code Online (Sandbox Code Playgroud)

做一个新的OutputStreamWriter给定OutputStream.Mockito知道模拟客户端套接字的输出流应该是什么样子,因为这setup部分中的这些行:

PipedOutputStream oStream = new PipedOutputStream();                 
when(mockTestClientSocket.getOutputStream()).thenReturn(oStream);
Run Code Online (Sandbox Code Playgroud)

另外,因为在setup测试之前发生了,所以我们知道我们的InputStream因为这一行而引用了这个OutputStream:

PipedInputStream iStream = new PipedInputStream(oStream);
Run Code Online (Sandbox Code Playgroud)

根据此构造函数的文档,此"创建一个PipedInputStream,以便它连接到管道输出流(oStream).写入(oStream)的数据字节将作为此流的输入提供."

while循环run()开始并导致"Hello,world!" 要发送出OutputStream(也由InputStream接收).接下来,我们很好地包装inputStream:

BufferedReader reader = new BufferedReader(new InputStreamReader(mockTestClientSocket.getInputStream(), DEFAULT_CHARSET)); 
Run Code Online (Sandbox Code Playgroud)

通过Mockito的魔力,该mockTestClientSocket.getInputStream()调用实际上iStream从之前返回,因为以下行:

when(mockTestClientSocket.getInputStream()).thenReturn(iStream);   
Run Code Online (Sandbox Code Playgroud)

所以现在我们有一个带有输入流的阅读器,并且该输入流被连接到输出流.该输出流被挂接到PrintWriter这是println荷兰国际集团"你好,世界!" S.然而,读者似乎永远不会得到ready().

为什么创建的监听器线程在创建期间挂起OutputStreamWriter,我的Hello, World!字符串如何从mocked socket正确发送到mocked客户端?

抱歉成为Mockito/java.net.*newb并且总体上有点厚.我想我已经包含了代码的所有相关部分,但是如果有什么不清楚请告诉我.

Nic*_*tto 6

我可以通过修改代码中的两件事来使您的单元测试通过:

1.mockServerSocket.accept()正确模拟

到目前为止,您模拟得mockServerSocket.accept()太早了,因为mockTestClientSocket尚未设置,因此它将返回null,您需要先设置它,因此您的代码应该是:

mockServerSocket = mock(ServerSocket.class);
// Set it first
mockTestClientSocket = mock(Socket.class);

try {
    // Then mock it
    when(mockServerSocket.accept()).thenReturn(mockTestClientSocket);
} catch (IOException e) {
    fail(e.getMessage());
}
...
Run Code Online (Sandbox Code Playgroud)

2. 正确同步你的线程

当您启动专用线程来管理客户端套接字时,您需要同步线程以确保您的消息可供读取。

为此,您可以:

  1. 通过调用来简化代码,reader.readLine()但这是一种阻塞方法,因为只要其他线程需要写入该行(此处的消息),当前线程就会等待。

代码可以是:

BufferedReader reader = ...

String receivedMessage = reader.readLine();
assertEquals("Hello, world!", receivedMessage);
mockTestClientSocket.close();
connection.closeSocket();
Run Code Online (Sandbox Code Playgroud)
  1. 设置一个TIMEOUT_TIME足够大甚至夸张的值,以确保另一个线程准备就绪,因此,例如,由于它是一个以纳秒为单位的值,因此您可以将其设置为30秒,以便30_000_000_000L. 如果您没有设置足够大的值,您的测试可能会在缓慢和/或过载和/或共享系统(例如用于持续集成的服务器)中不稳定。