代码之家  ›  专栏  ›  技术社区  ›  Ian Terrell

Java多线程网络应用程序的单元测试

  •  0
  • Ian Terrell  · 技术社区  · 6 年前

    我正在编写一个Java多线程网络应用程序,并且很难想出一种方法来测试从网络客户端发送和接收通信的对象。

    对象向多个客户机发送消息,然后等待客户机的响应。

    当每个客户机响应时,都会更新仪表板样式的GUI。

    更详细地说…

    message对象表示要发送的文本消息,并包含应接收该消息的客户端数组。

    消息对象负责将自己分派给所有适当的客户机。

    当对消息对象调用dispatch()方法时,该对象为客户机数组中的每个客户机生成一个新线程(messagedispatcher)。

    每个消息调度程序:

    • 向客户端打开新的TCP套接字(套接字)

    • 将消息传递到其客户端…printwriter out.println(消息文本)

    • 创建一个“status”对象,该对象被传递到消息对象中的队列,然后再传递到GUI。

    每个状态对象表示以下事件之一:

    • 消息传递到套接字(通过printwriter out.println())

    • 显示从客户端收到的收据(通过bufferedreader/inputstreamreader in.readline().. 在收到网络输入之前阻止 )

    • 从客户处收到的用户确认收据(通过与上述相同的方法)

    所以……我想对消息对象进行单元测试。(使用JUnit)

    单元测试称为messagetest.java(包含在下面)。

    我的第一步是用单个收件人设置邮件对象。

    然后,我使用jmockit创建一个模拟套接字对象,该对象可以将模拟输出流对象(我使用的是扩展输出流的bytearrayOutputstream)提供给PrintWriter。

    然后,当messageDispatcher调用(printWriter对象).out时,消息文本将理想地传递给我的模拟套接字对象(通过模拟输出流),该对象可以检查消息文本是否正常。

    以及InputStreamReader的示例原则……mock socket对象还提供一个mock inputstreamreader对象,该对象提供一个mock bufferedreader,由messageDispatcher调用(如前所述,.readline()中的messageDispatcher阻塞)。此时,模拟缓冲阅读器应该向消息调度程序提供一个假确认…

    // mock Socket
    Mockit.redefineMethods(Socket.class, new Object()
    {
    
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        ByteArrayInputStream input = new ByteArrayInputStream();
    
        public OutputStream getOutputStream()
        {
            return output;
        }
    
        public InputStream getInputStream()
        {
            return input;
        }
    
    });
    

    如果这不是多线程的,那么一切都可以正常工作。但是,我不知道如何用多个线程来实现这一点。有人能给我一些建议或小费吗?

    此外,如果您对设计有任何输入(例如,消息对象负责自己的传递,而不是单独的传递对象)。依赖注入——每一个客户端交付的样式/独立线程),那么我也会感兴趣。

    更新:代码如下:

    Java语言

    public class Message {
    
        Client[] to;
    
        String contents;
    
        String status;
    
        StatusListener listener;
    
        BlockingQueue<Status> statusQ;
    
        public Message(Client[] to, String contents, StatusListener listener) 
        {
            this.to = to;
            this.contents = contents;
            this.listener = listener;
        }
    
        public void dispatch()
        {
            try {
    
                // open a new thread for each client
    
                // keep a linked list of socket references so that all threads can be closed
                List<Socket> sockets = Collections.synchronizedList(new ArrayList<Socket>());
    
                // initialise the statusQ for threads to report message status
                statusQ = new ArrayBlockingQueue<Status>(to.length*3); // max 3 status objects per thread
    
                // dispatch to each client individually and wait for confirmation
                for (int i=0; i < to.length; i++) {
    
                System.out.println("Started new thread");
    
                (new Thread(new MessageDispatcher(to[i], contents, sockets, statusQ))).start();
    
                }
    
                // now, monitor queue and empty the queue as it fills up.. (consumer)
                while (true) {
                    listener.updateStatus(statusQ.take());
                }
            }
    
            catch (Exception e) { e.printStackTrace(); }
    
        }
    
        // one MessageDispatcher per client
        private class MessageDispatcher implements Runnable
        {
    
            private Client client;
            private String contents;
            private List<Socket> sockets;
            private BlockingQueue<Status> statusQ;
    
            public MessageDispatcher(Client client, String contents, List<Socket> sockets, BlockingQueue<Status> statusQ) {
    
                this.contents = contents;
    
                this.client = client;
    
                this.sockets = sockets;
    
                this.statusQ = statusQ;
    
            }
    
            public void run() {
    
            try {
    
                // open socket to client
                Socket sk = new Socket(client.getAddress(), CLIENTPORT);
    
                // add reference to socket to list
                synchronized(sockets) {
                    sockets.add(sk);
                }
    
                PrintWriter out = new PrintWriter(sk.getOutputStream(), true);
    
                BufferedReader in = new BufferedReader(new InputStreamReader(sk.getInputStream()));
    
                // send message
                out.println(contents);
    
                // confirm dispatch
                statusQ.add(new Status(client, "DISPATCHED"));
    
                // wait for display receipt
                in.readLine();
    
                statusQ.add(new Status(client, "DISPLAYED"));
    
                // wait for read receipt
                in.readLine();
    
                statusQ.add(new Status(client, "READ"));
    
                }
    
                catch (Exception e) { e.printStackTrace(); }
            }
    
        }
    
    }
    

    …以及相应的单元测试:

    消息测试.java

    public class MessageTest extends TestCase {
    
        Message msg;
    
        static final String testContents = "hello there";
    
        public void setUp() {
    
            // mock Socket
            Mockit.redefineMethods(Socket.class, new Object()
            {
    
                ByteArrayOutputStream output = new ByteArrayOutputStream();
                ByteArrayInputStream input = new ByteArrayInputStream();
    
                public OutputStream getOutputStream()
                {
                    return output;
                }
    
                public InputStream getInputStream()
                {
                    return input;
                }
    
    
            });
    
            // NB
            // some code removed here for simplicity
            // which uses JMockit to overrides the Client object and give it a fake hostname and address
    
            Client[] testClient = { new Client() };
    
            msg = new Message(testClient, testContents, this);
    
        }
    
        public void tearDown() {
        }
    
        public void testDispatch() {
    
            // dispatch to client
            msg.dispatch();
    
    
        }   
    }
    
    2 回复  |  直到 16 年前
        1
  •  1
  •   Ryan    16 年前

    注意,多个消息(多播)的发送可以通过NIO API(Java.NIO)在一个阻塞方法中实现,而不需要创建新线程。不过,NIO相当复杂。

    我将首先编写测试,使用一个测试定义的statusListener实现,它将所有更新事件存储在一个列表中。当dispatch()方法返回时,测试可以在事件列表的状态上执行断言。

    使用线程或NIO是消息类的实现细节。因此,除非您不介意将测试耦合到这个实现细节,否则我建议引入一个助手类,该类负责发送多个异步消息,并在任何异步响应时通知消息对象。然后,您可以在单元测试中模拟助手类,而不将它们耦合到线程或NIO。

    我成功地为向一个客户机发送消息的情况实现了一个测试。我还对原始生产代码进行了一些更改,如下所示:

    public class Message
    {
       private static final int CLIENT_PORT = 8000;
    
       // Externally provided:
       private final Client[] to;
       private final String contents;
       private final StatusListener listener;
    
       // Internal state:
       private final List<Socket> clientConnections;
       private final BlockingQueue<Status> statusQueue;
    
       public Message(Client[] to, String contents, StatusListener listener)
       {
          this.to = to;
          this.contents = contents;
          this.listener = listener;
    
          // Keep a list of socket references so that all threads can be closed:
          clientConnections = Collections.synchronizedList(new ArrayList<Socket>());
    
          // Initialise the statusQ for threads to report message status:
          statusQueue = new ArrayBlockingQueue<Status>(to.length * 3);
       }
    
       public void dispatch()
       {
          // Dispatch to each client individually and wait for confirmation:
          sendContentsToEachClientAsynchronously();
    
          Status statusChangeReceived;
    
          do {
             try {
                // Now, monitor queue and empty the queue as it fills up (consumer):
                statusChangeReceived = statusQueue.take();
             }
             catch (InterruptedException ignore) {
                break;
             }
          }
          while (listener.updateStatus(statusChangeReceived));
    
          closeRemainingClientConnections();
       }
    
       private void closeRemainingClientConnections()
       {
          for (Socket connection : clientConnections) {
             try {
                connection.close();
             }
             catch (IOException ignore) {
                // OK
             }
          }
    
          clientConnections.clear();
       }
    
       private void sendContentsToEachClientAsynchronously()
       {
          for (Client client : to) {
             System.out.println("Started new thread");
             new Thread(new MessageDispatcher(client)).start();
          }
       }
    
       // One MessageDispatcher per client.
       private final class MessageDispatcher implements Runnable
       {
          private final Client client;
    
          MessageDispatcher(Client client) { this.client = client; }
    
          public void run()
          {
             try {
                communicateWithClient();
             }
             catch (IOException e) {
                throw new RuntimeException(e);
             }
          }
    
          private void communicateWithClient() throws IOException
          {
             // Open connection to client:
             Socket connection = new Socket(client.getAddress(), CLIENT_PORT);
    
             try {
                // Add client connection to synchronized list:
                clientConnections.add(connection);
    
                sendMessage(connection.getOutputStream());
                readRequiredReceipts(connection.getInputStream());
             }
             finally {
                connection.close();
             }
          }
    
          // Send message and confirm dispatch.
          private void sendMessage(OutputStream output)
          {
             PrintWriter out = new PrintWriter(output, true);
    
             out.println(contents);
             statusQueue.add(new Status(client, "DISPATCHED"));
          }
    
          private void readRequiredReceipts(InputStream input) throws IOException
          {
             BufferedReader in = new BufferedReader(new InputStreamReader(input));
    
             // Wait for display receipt:
             in.readLine();
             statusQueue.add(new Status(client, "DISPLAYED"));
    
             // Wait for read receipt:
             in.readLine();
             statusQueue.add(new Status(client, "READ"));
          }
       }
    }
    public final class MessageTest extends JMockitTest
    {
       static final String testContents = "hello there";
       static final String[] expectedEvents = {"DISPATCHED", "DISPLAYED", "READ"};
    
       @Test
       public void testSendMessageToSingleClient()
       {
          final Client theClient = new Client("client1");
          Client[] testClient = {theClient};
    
          new MockUp<Socket>()
          {
             @Mock(invocations = 1)
             void $init(String host, int port)
             {
                assertEquals(theClient.getAddress(), host);
                assertTrue(port > 0);
             }
    
             @Mock(invocations = 1)
             public OutputStream getOutputStream() { return new ByteArrayOutputStream(); }
    
             @Mock(invocations = 1)
             public InputStream getInputStream()
             {
                return new ByteArrayInputStream("reply1\nreply2\n".getBytes());
             }
    
             @Mock(minInvocations = 1) void close() {}
          };
    
          StatusListener listener = new MockUp<StatusListener>()
          {
             int eventIndex;
    
             @Mock(invocations = 3)
             boolean updateStatus(Status status)
             {
                assertSame(theClient, status.getClient());
                assertEquals(expectedEvents[eventIndex++], status.getEvent());
                return eventIndex < expectedEvents.length;
             }
          }.getMockInstance();
    
          new Message(testClient, testContents, listener).dispatch();
       }
    }

    上面的jmockit测试使用了 MockUp 类,在最新版本中尚不可用。它可以换成 Mockit.setUpMock(Socket.class, new Object() { ... }) 但是。

        2
  •  1
  •   tster    16 年前

    也许您可以在消息类中使用AbstractFactory来创建输出流和输入流,而不是重新定义getOutputStream和getInputStream方法。在正常操作中,工厂将使用一个插座。然而,对于测试来说,给它一个工厂,让它成为你选择的流程。这样你就可以更好地控制正在发生的事情。