使用 Mockito 模拟服务器-客户端连接

Posted

技术标签:

【中文标题】使用 Mockito 模拟服务器-客户端连接【英文标题】:Mocking a server-client connection with Mockito 【发布时间】:2017-03-17 19:55:51 【问题描述】:

简介

我正在尝试通过将字符串从一个线程发送到另一个线程来测试套接字连接,其中服务器和客户端套接字使用 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());
    

测试一直运行到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());
    

我要测试的部分内容是在connection.listen() 中启动的内部类中的以下run()

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;
     

这是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();
    
    ...

它在我的脑海中是如何运作的

我将尝试逐步完成我的测试过程,为这个问题添加一些额外的上下文。从testConnection()开头开始:

TcpSocketConnection connection = new TcpSocketConnection(mockServerSocket);

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

clientSocket = socket.accept();

由于上面的socket 是对mockServerSocket 的引用,因此Mockito 知道返回对名为mockTestClientSocket 的模拟套接字的引用:

when(mockServerSocket.accept()).thenReturn(mockTestClientSocket);

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

writer = new OutputStreamWriter(clientSocket.getOutputStream(), DEFAULT_CHARSETNAME);

在给定OutputStream 的情况下创建一个新的OutputStreamWriter。由于setup 部分中的这些行,Mockito 知道模拟客户端套接字的输出流应该是什么样子:

PipedOutputStream oStream = new PipedOutputStream();                 
when(mockTestClientSocket.getOutputStream()).thenReturn(oStream);

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

PipedInputStream iStream = new PipedInputStream(oStream);

根据此构造函数的文档,This “创建一个 PipedInputStream,以便将其连接到管道输出流 (oStream)。然后写入 (oStream) 的数据字节将可用作此流的输入。 "

run() 中的 while 循环开始并导致“Hello, world!”被发送到 OutputStream (也被 InputStream 接收)。 接下来,我们将 inputStream 包装好:

BufferedReader reader = new BufferedReader(new InputStreamReader(mockTestClientSocket.getInputStream(), DEFAULT_CHARSET)); 

借助 Mockito 的魔力,mockTestClientSocket.getInputStream() 调用实际上返回了之前的 iStream,因为以下行:

when(mockTestClientSocket.getInputStream()).thenReturn(iStream);   

所以现在我们有了一个带有输入流的阅读器,并且该输入流连接到一个输出流。该输出流连接到PrintWriter,即printlning "Hello,world!"s。然而,读者似乎从来没有得到过ready()

问题

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

很抱歉,我是一个 Mockito/java.net.* newb 并且总体上有点厚。我想我已经包含了所有相关的代码部分,但如果有任何不清楚的地方请告诉我。

【问题讨论】:

mockServerSocket = mock(ServerSocket.class);尝试 when(mockServerSocket.accept()).thenReturn(mockTestClientSocket); . . . mockTestClientSocket = mock(Socket.class);.... 在您告诉 Mockito 返回它之后,您正在定义客户端套接字模拟。所以返回的引用不是你所期望的。颠倒这两个并重试。 我同意@AshleyFrieze,这是您的主要问题,但如果这仍然没有改变模拟套接字的行为,请尝试其他语法:` doReturn(oStream).when(mockTestClientSocket) .getOutputStream () ;` (注意从方法调用后面移动到点之前的右括号。)在模拟抽象或真实类(而不是接口)时,when().thenReturn() 形式有时会调用真实方法并伪造结果。 doReturn().when() 表单从不调用真正的方法。 这个问题解决了吗?我在问,因为您已将 @nicholas 的答案标记为已接受,但由于赏金仍然开放 是的,Nicolas 的回答是我需要让这个测试工作的建议。赏金还有等待期。 【参考方案1】:

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

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

...

2。正确同步你的线程

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

为此,您可以:

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

代码可能是:

BufferedReader reader = ...

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

【讨论】:

以上是关于使用 Mockito 模拟服务器-客户端连接的主要内容,如果未能解决你的问题,请参考以下文章

关于单元测试的那些事儿,Mockito 都能帮你解决

无法模拟 Glassfish Jersey 客户端响应对象

使用 Mockito 模拟服务器

如何使用 Mockito 框架模拟我的服务

WCF 模拟和 SQL 可信连接?

如何使用 mockito 模拟方法?