Hi guys,<div><br></div><div>I've been trying to implement the binary protocol used in JXTA using Netty for the past week or so (mixed in with trying to implement an HTTP tunnel that doesn't use servlets, but that's parked for the moment). In doing this I've been trying to unit test my channel handlers, which I have found to be quite challenging. I'm starting to wonder whether this is because I am taking the wrong approach. First, I'll give a brief overview of the protocol:</div>
<div><br></div><div>1. When a connection is opened, I must send a "welcome" message. This contains some dynamic data used to identify myself, such as a unique ID, the version of the protocol I am using and some meta-data about how the connection should be used.</div>
<div>2. While this is happening, the peer on the other end will be sending their welcome message. This message ends with CRLF.</div><div>3. If I don't receive a full welcome message from the other peer within 5 seconds, or if it is invalid in some way (wrong protocol version, unsupported modes specified in the meta-data, etc), then I must close the connection.</div>
<div>4. I cannot send any messages to the other peer until I have received a full, valid welcome message from them.</div><div>5. Once this welcome message handshake is complete, I can send messages to the peer. These messages are structured somewhat like HTTP requests, except in binary with known binary keys mapping to known binary values. One of these "headers" specifies the length of the rest of the message, so as a receiver I can predict how long the message is, preallocate a buffer of the correct size, read the message and then process it.</div>
<div>6. Either peer can close the connection at the end of any message, without any kind of "goodbye" message required.</div><div><br></div><div>In order to deal with this protocol I've created a ChannelHandler, extending SimpleChannelHandler.</div>
<div><br></div><div>1. I override the channelConnected() method in order to send my welcome message, and I do not allow the connected event to bubble up the pipeline at this point. </div><div>2. I override the messageReceived() method to process the welcome message, and call channel.close() within this if the welcome message is invalid or not what I expect. Once I receive a full welcome message, I fire my own UpstreamChannelStateEvent to state we are connected (properly). I actually don't know if this is "legal" or best practice.</div>
<div>3. I override the writeRequested() method to perform the framing of the ChannelBuffers containing full messages passed down from the previous handler which has done the serialization.</div><div><br></div><div>Now, unit testing this has proved challenging. For instance, take the simple unit test of "when channelConnected() is called, write a welcome message to the channel". The implementation of channelConnected is:</div>
<div><br></div><div><div><font class="Apple-style-span" face="'courier new', monospace"><span class="Apple-style-span" style="font-size: small;">public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {</span></font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"><span class="Apple-style-span" style="font-size: small;"> SocketAddress remoteAddress = ctx.getChannel().getRemoteAddress();</span></font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"><span class="Apple-style-span" style="font-size: small;"> EndpointAddress destAddr = new EndpointAddress(endpointAddr.getProtocolName(), remoteAddress.toString(), null, null);</span></font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"><span class="Apple-style-span" style="font-size: small;"> WelcomeMessage welcome = new WelcomeMessage(destAddr, endpointAddr, peerId, false);</span></font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"><span class="Apple-style-span" style="font-size: small;"> ChannelBuffer welcomeBytes = ChannelBuffers.copiedBuffer(welcome.getByteBuffer());</span></font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"><span class="Apple-style-span" style="font-size: small;"> Channels.write(ctx, Channels.future(ctx.getChannel()), welcomeBytes);</span></font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"><span class="Apple-style-span" style="font-size: small;">}</span></font></div><div><font class="Apple-style-span" face="'courier new'" size="2"><span class="Apple-style-span" style="font-size: 10px;"><br>
</span></font></div><div>My approach so far to unit testing this has been to use jMock 2, producing tests blocks like this:</div><div><br></div><div><div><font class="Apple-style-span" face="'courier new', monospace">@Test</font></div>
<div><font class="Apple-style-span" face="'courier new', monospace">public void testSendsWelcomeMessageImmediately() throws Exception {</font></div><div><font class="Apple-style-span" face="'courier new', monospace"> final UpstreamChannelStateEvent ev = new UpstreamChannelStateEvent(channel, ChannelState.CONNECTED, true);</font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"> final WelcomeMessage expectedWelcomeMessage = new WelcomeMessage(REMOTE_ENDPOINT_ADDR, LOCAL_ENDPOINT_ADDR, LOCAL_PEER_ID, false);</font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"> </font></div><div><font class="Apple-style-span" face="'courier new', monospace"> mockContext.checking(new Expectations() {{</font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"> ignoring(ctx).getChannel(); will(returnValue(channel));</font></div><div><font class="Apple-style-span" face="'courier new', monospace"> atLeast(1).of(channel).getRemoteAddress(); will(returnValue(REMOTE_SOCK_ADDR));</font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"> one(ctx).sendDownstream(with(aDownstreamMessageEvent(aWelcomeMessage(expectedWelcomeMessage))));</font></div><div><font class="Apple-style-span" face="'courier new', monospace"> }});</font></div>
<div><font class="Apple-style-span" face="'courier new', monospace"> </font></div><div><font class="Apple-style-span" face="'courier new', monospace"> handler.channelConnected(ctx, ev);</font></div>
<div><font class="Apple-style-span" face="'courier new', monospace">}</font></div><div><font class="Apple-style-span" face="'courier new'" size="2"><span class="Apple-style-span" style="font-size: 10px;"><br>
</span></font></div><div>The real sticking point was the "aDownstreamMessageEvent" and "aWelcomeMessage" Matchers I had to write by hand. The various event types in Netty do not implement equals() so I cannot do something like:</div>
<div><br></div><div><font class="Apple-style-span" face="'courier new', monospace">DownstreamMessageEvent expectedEvent = new DownstreamMessageEvent(...);</font></div><div><font class="Apple-style-span" face="'courier new', monospace">one(ctx).sendDownstream(with(equals(expectedDownStreamMessage));</font></div>
<div><br></div><div>Additionally, in other test cases where I want to assert that my code fires events like an UpstreamChannelStateEvent with State=CONNECTED, Value=TRUE, I can't do this using equals() either. Instead, I must write custom matchers to compensate for this. All this results in, for a simple method like channelConnected(), roughly 5-6 times the amount of code required to write a test compared to the code itself (aggravated by most aspects of JXTA being difficult to test too, and using NIO ByteBuffers rather than ChannelBuffers... sigh). This will get easier over time as I start building more matchers for jmock specifically for Netty, but I still can't help feeling that there must be an easier way to test ChannelHandlers in isolation.</div>
<div><br></div><div>Do you guys have any advice on how best to test Netty code?</div><div>Iain</div></div></div>