Search code examples
javajava-ee-7apache-tomeejava-websockettomee-7

How to test a Java EE7 Websocket


I have implemented a WebSocket using the api provided with Java EE 7. Additionally I have implemented a client that requests my WebSocket without any problems. To be sure this remains working when performing some code changes I want to implement tests which can also be run on a build server like e.g. Jenkins CI and not only locally. I am using maven.

Here's my server endpoint:

import javax.enterprise.context.ApplicationScoped;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@ApplicationScoped
@ServerEndpoint("/example")
public class WebSocket {

    private final Set<Session> sessions = Collections.synchronizedSet(new HashSet<>());

    @OnOpen
    public void open(Session session) {
        sessions.add(session);
    }

    @OnClose
    public void close(Session session) {
        sessions.remove(session);
    }

    @OnError
    public void onError(Throwable error) {
        //TODO do something
    }

    @OnMessage
    public void handleMessage(String message, Session session) throws IOException {
        session.getBasicRemote().sendText("Hello "+message+"!");
    }
}

Here's my client endpoint:

import javax.websocket.*;
import java.io.IOException;
import java.net.URI;

@ClientEndpoint
public class WebsocketClient {

    private String uri = "ws://localhost:8181/<some-path>/example";
    private Session userSession = null;
    private String answer;

    public WebsocketClient() {
        try {
            WebSocketContainer container = ContainerProvider
                .getWebSocketContainer();
            container.connectToServer(this, new URI(uri));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public boolean isClosed() {
        return userSession == null || !userSession.isOpen();
    }

    @OnOpen
    public void onOpen(Session userSession) {
        this.userSession = userSession;
    }

    @OnClose
    public void onClose(Session userSession, CloseReason reason) {
        this.userSession = null;
    }

    @OnMessage
    public void onMessage(String message) {
        this.answer = message;
    }

    public String getAnswer() {
        return answer;
    }

    public void sendMessage(String message) {
        if (userSession != null && userSession.isOpen())
            try {
                this.userSession.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        else {
            System.out.println("Session closed!");
        }
    }

    public void reset() {
        this.answer = null;
    }
}

I also have some other classes that are tested on the build server that use CDI. To be able to use the CDI and other application container functionality I am using the EJBContainerRunner with apache TomeEE/OpenEJB in version 7.0.0-M3 which starts an embedded TomEE runs my tests and shuts it down afterwards. Using this approach I don't have an URL to call. This works fine with e.g. JAX-RS REST services where you can simply call your classes methods in your tests and check the responses. But how do you do something like this with WebSockets? I can't simply call the methods due to the missing Session.

My current test looks like this:

import org.apache.openejb.junit.jee.EJBContainerRunner;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.net.SocketException;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;

@RunWith(EJBContainerRunner.class)
public class TestWebsocket {

    private WebsocketClient socket;

    @Test
    public void test() throws SocketException {
        String answer = requestSynchronous("Java");
        assertThat(answer, is(equalTo("Hello Java!")));
    }

    public String requestSynchronous(String message) throws SocketException {
        if (socket == null || socket.isClosed()) {
             socket = new WebsocketClient();
        }
        socket.reset();
        socket.sendMessage(message);
        String answer = null;
        int i = 0;
        while (answer == null && i < 10000) {
            try {
                answer = socket.getAnswer();
                Thread.sleep(1);
                i++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (answer != null)
            return answer;
        else throw new SocketException("Connection timeout");
    }

}

This works running a TomEE on my local machine with e.g. the tomee-maven-plugin. But this approach can't be done on the build server. Is there a simpler way than configuring special ports or setting up a virtual machine for testing?

Thanks in advance!


Solution

  • I found a solution for my testing problem without mocking, using a vm, ...

    For sending an answer message to the client it is not needed to call session.getBasicRemote().sendText("...");. Instead return the message you want to send.

    The handleMessage method in the class WebSocket changes to:

    @OnMessage
    public String handleMessage(String message){
        return "Hello "+message+"!";
    }
    

    And the test simplifies to:

    @RunWith(EJBContainerRunner.class)
    public class TestWebsocket {
    
        @Inject
        private WebSocket socket;
    
        @Test
        public void test() throws SocketException {
            String answer = socket.handleMessage("Java");
            assertThat(answer, is(equalTo("Hello Java!")));
        }   
    }
    

    This approach doesn't send messages to the outside of the web application container that is started for the test. This makes it much easier to handle the test.