Blather is a Netty-based Java websocket client, with a test harness that makes it trivial to write JUnit tests for websocket communication.
It emphasizes simplicity and functional interfaces. Code being worth a thousand words, here is a trivial client that just connects, sends "hello" and then "goodbye " with whatever response it got appended:
public class WebsocketTest {
public static void main(String[] args) {
Blather.create().client("ws://foo.example/websocket").sendOnConnect("hello")
.onMessage(String.class, WebsocketTest::onMessage);
}
public static Object onMessage(int msgIndex, String inboundMessage, ChannelControl ctrl) {
return "goodbye " + inboundMessage;
}
}
Note the use of JDK 8 member references to separate the logic of connecting from the code that has the conversation.
To use, add the Maven repository as described here and
<dependency>
<artifactId>blather</artifactId>
<groupId>com.mastfrog</groupId>
<version>2.0.1-dev</version>
</dependency>
The ChannelControl
interface passed into all callbacks allows you to:
- Asynchronously send messages to the server, rather than sending them as the return value
- Close the connection (politely, sending a close frame)
- Replace the current handler with another one - so if your connection has various
modes - perhaps its own handshaking phase followed by other communication - you can
simply hand off message handling to a different handler by calling
ChannelControl.nextCallback()
.
Blather uses Jackson for data marshalling for non-string
types (note that Jackson can speak more than JSON - for example, Bson4Jackson).
So, you'll note that the message handler method above simply returned Object
- you have
flexibility here - you can return
- A
String
or anyCharSequence
, which will be marshalled to a plain text websocket frame - A Netty
WebSocketFrame
, which will be sent as-is - Any other object, which will be marshalled using Jackson (you can supply and configure the
ObjectMapper
using oen of the other factory methods onBlather
. - Directly get the Netty
Channel
object to do as you wish with
One of the main use cases is writing unit- or functional-tests, so out of the box exception handling is dealt with in a particular way:
-
In a test, simply call
WebsocketClientRequest.await()
, and any exception thrown will cause the connection to be closed, and the exception will be rethrown in the main thread (any subsequent exceptions will show up as suppressed exceptions in the stack trace of the first) -
For other environments (where you don't have a thread blocked waiting for request completion - this is an asynchronous library after all), provide a
WebsocketErrorHandler
toWebsocketClientRequest.withErrorHandler()
and that can decide whether to close the connection or not.
There is a test-jar
Maven artifact which includes a test harness for writing tests which
automatically start a server. While designed with Acteur in
mind, the Server
and ServerControl
interfaces are
trivial to implement.
Here is a test which starts a small Acteur server, makes a websocket connection, does some stuff and closes it:
@RunWith(GuiceRunner.class)
@TestWith(TestApplication.Module.class)
public class HarnessTest {
int count = 0;
@Test(timeout=20000)
public void test(WebsocketHostClient client) throws Throwable {
client.request("/ws")
.log()
.addHeader(Headers.stringHeader("X-Foo"), "bar")
.addUrlQueryPair("foo", "bar")
.onMessage(String.class, this::withFrame)
.await(Duration.ofSeconds(20));
}
public Object withFrame(int msgIndex, String data, ChannelControl ctrl) {
if (count++ == 4) {
ctrl.close();
fail("Handler should have been replaced");
return null;
} else if (count == 3) {
ctrl.nextCallback(this::bypass, String.class);
}
return "Hello: " + data;
}
public Object bypass(int msgIndex, String data, ChannelControl ctrl) {
System.out.println("BYPASS GOT " + data);
ctrl.close();
return null;
}
}
The test harness also has the ability, with Acteur (or anything that wants to
inject an ErrorInterceptor
and call it on errors) to catch server-side exceptions
and rethrow those at the end of a test, so that server side errors are not opaque
to the test, and they simply show up as test failures with a stack trace.