Unit test framework for channels?

Jesse Hutton jesse.hutton at gmail.com
Tue May 10 11:20:40 EDT 2011


On Mon, May 9, 2011 at 3:03 PM, Steve Atkinson
<steven.atkinson at kaazing.com>wrote:

> Hi there.
>
> I'm writing some unit tests for channel handlers, and I am considering
> building  fake channels, pipelines, an upstream product queue and a
> downstream channel buffer.
>
> I need to be able to send and receive custom events and assert over product
> queues and bytes written.
>
> Is there anything like this besides the CodecEmbedder - I'm planning to
> whip up something like that but for general channel handler testing not just
> codecs...
>

I've had some success with unit testing channel handlers and custom events
in pipelines.

For a channel handler, the basic technique is to mock the Channel and
ChannelHandlerContext, then instantiate your handler with whatever
dependencies it requires with mocks, and lastly, create a stub MessageEvent,
containing the message your handler expects. Here's a small test example
using JMock:

public class ExampleChannelHandlerTest {

    private Mockery context = new JUnit4Mockery() {
        {
            setImposteriser(ClassImposteriser.INSTANCE);
        }
    };

    private ChannelHandlerContext mockCtx;
    private Channel mockChannel;
    private ChannelSender mockChannelSender;
    private RemoteUpdateProvider mockUpdateProvider;
    private MessagingHandler mockMessageProcessor;
    private MyAppServerHandler mockMyAppHandler;
    private RemoteServiceChannelHandler channelHandler;
    private String clientIp = "172.0.0.1";
    int protocolVersion = 1;

    @Before
    public void setup() {
        mockCtx = context.mock(ChannelHandlerContext.class);
        mockChannel = context.mock(Channel.class);
        mockMyAppHandler = context.mock(MyAppServerHandler.class);
        mockUpdateProvider = context.mock(RemoteUpdateProvider.class);
        mockMessageProcessor = context.mock(MessagingHandler.class);
        mockChannelSender = context.mock(ChannelSender.class);
        channelHandler = new RemoteServiceChannelHandler(
                mockUpdateProvider, mockMessageProcessor,
mockChannelSender);
    }

    @Test
    public void testPingEventHandlerOnReceiveSendsCorrectPingMessage()
throws PacketException {
        final long serial = 33333;
        final int versionCode = 273843;
        final int typeCode = 231111;
        final UpdateResult result = UpdateResult.newBuilder()
            .setStatus(UpdateResult.ResponseStatus.NO_RESULTS_INCLUDED)
            .build();

        VersionInfo vi = new VersionInfo(versionCode);
        PacketPing ping = new PacketPing(serial, typeCode,
                vi.major(), vi.minor(), vi.build(), protocolVersion);
        PingEvent executionEvent = new PingEvent(ping, mockMyAppHandler,
clientIp);

        final PingMessage message = PingMessage.newBuilder()
                .setRemoteIp(clientIp)
                .setSerialNumber(ping.getSerialNumber())
                .setVersionCode(ping.getVersionCode())
                .setTypeCode(ping.getTypeCode())
                .build();

        context.checking(new Expectations() {
            {
                oneOf(mockMessageProcessor).onPingEvent(message);
                oneOf(mockUpdateProvider).getFirmwareUpdate(serial,
                        versionCode, typeCode, "172.0.0.1");
                will(returnValue(result));
                oneOf(mockMyAppHandler).getProtocolVersion();
                will(returnValue(protocolVersion));
                oneOf(mockChannelSender).sendAndForget(mockCtx, mockChannel,
                        new PacketAck(ResponseCode.OK, protocolVersion));
                oneOf(mockMyAppHandler).onResponseSent(
                        new PacketAck(ResponseCode.OK, protocolVersion));
                oneOf(mockMessageProcessor).onPingEvent(message);
            }
        });

        MessageEvent e = getMessageStub(executionEvent);
        channelHandler.messageReceived(mockCtx, e);
    }

    private MessageEvent getMessageStub(final Object message) {
        return new MessageEvent() {
            @Override
            public Object getMessage() {
                return message;
            }
            @Override
            public SocketAddress getRemoteAddress() {
                return null;
            }
            @Override
            public Channel getChannel() {
                return mockChannel;
            }
            @Override
            public ChannelFuture getFuture() {
                return null;
            }
        };
    }
}

RemoteServiceChannelHandler is an upstream ExecutionHandler.
MyAppServerHandler, in this case, is a non-sharable handler that sits
further downstream and deals with most of the client/server state. When it
receives, a PingPacket from the client, it creates a PingEvent, which is
sent upstream to the RemoteServiceChannelHandler (passing itself to provide
callback for the execution handler). The other mocks represent a client
interface for checking for remote updates, a channel writer helper, and an
interface for JMS messaging. I've found it pretty easy to test various code
paths of a handler using this technique.

Testing a pipeline is a bit messier. Here's a trimmed down example:

public class ExamplePipelineTest {
    private static ExecutionHandler realExecutionHandler = new
ExecutionHandler(
            new OrderedMemoryAwareThreadPoolExecutor(4, 24288, 24288));

    private Mockery context = new JUnit4Mockery() {
        {
            setImposteriser(ClassImposteriser.INSTANCE);
        }
    };

    /**
     * Override these methods to return mocks to test expectations.
     */
    private class MyAppHandlerProviderStub implements ChannelHandlerProvider
{
        public PacketDecoder getPacketDecoder() {
            return realDecoder;
        }
        public PacketEncoder getPacketEncoder() {
            return realEncoder;
        }
        public ReadTimeoutHandler getReadTimeoutHandler() {
            return readTimeoutHandler;
        }
        public MyAppServerHandler getMyAppServerHandler() {
            return realServerHandler;
        }
        public ExecutionHandler getExecutionHandler() {
            return realExecutionHandler;
        }
        public RemoteServiceChannelHandler getRemoteServiceChannelHandler()
{
            return realRemoteServiceHandler;
        }
        public RemoteProgressHandler getRemoteProgressHandler() {
            return realRemoteProgressHandler;
        }
    }

    // mocks
    private RemoteUpdateProvider mockRemoteUpdateProvider;
    private MessagingHandler mockMessageProcessor;
    private Channel mockChannel;
    private ChannelSink mockSink;
    private ChannelSender mockChannelSender;

    // real implementations
    private String clientIp = "172.0.0.1";
    int protocolVersion = 1;
    private PacketEncoder realEncoder;
    private PacketDecoder realDecoder;
    private ReadTimeoutHandler readTimeoutHandler;
    private MyAppServerHandler realServerHandler;

    private RemoteServiceChannelHandler realRemoteServiceHandler;
    private RemoteProgressHandler realRemoteProgressHandler;
    private ChannelGroupHandler realChannelGroupHandler;
    private MyAppPipelineFactory pipelineFactory;
    private ChannelPipeline pipeline;
    private static Timer timer = new HashedWheelTimer();
    private ChannelConfig channelConfig = new DefaultServerChannelConfig();

    @AfterClass
    public static void stopTimer() {
        timer.stop();
        realExecutionHandler.releaseExternalResources();
    }

    @Before
    public void setup() throws Exception {
        realEncoder = new PacketEncoder();
        realDecoder = new PacketDecoder();
        readTimeoutHandler = new ReadTimeoutHandler(timer, 30);
        mockChannelSender = context.mock(ChannelSender.class);
        realServerHandler = new MyAppServerHandler(
                new UpstreamEventDispatcher(), mockChannelSender);
        realServerHandler.setRemoteIp(clientIp);

        mockRemoteUpdateProvider = context.mock(RemoteUpdateProvider.class);
        mockMessageProcessor = context.mock(MessagingHandler.class);

        realRemoteServiceHandler = new RemoteServiceChannelHandler(
                mockRemoteUpdateProvider, mockMessageProcessor,
                mockChannelSender);

        realRemoteProgressHandler = new RemoteProgressHandler(
                mockMessageProcessor, mockChannelSender);

        mockChannel = context.mock(Channel.class);
        mockSink = context.mock(ChannelSink.class);

        pipelineFactory = new MyAppPipelineFactory(
                new MyAppHandlerProviderStub());
    }

    @Test
    public void testPipelineOnPing() throws Exception {
        pipeline = pipelineFactory.getPipeline();
        pipeline.attach(mockChannel, mockSink);

        context.checking(new Expectations() {
            {
                oneOf(mockChannel).getConfig();
                will(returnValue(channelConfig));
                exactly(3).of(mockChannel).getRemoteAddress();
                will(returnValue(null));

                exactly(3).of(mockChannel).isOpen();
                will(returnValue(true));
                oneOf(mockChannel).isReadable();
                will(returnValue(true));

                // finally passed to upstream handler
                oneOf(mockMessageProcessor).onPingEvent(
                        with(any(PingMessageProto.PingMessage.class)));
                oneOf(mockRemoteUpdateProvider).getFirmwareUpdate(1882993,
                        65536, 1, clientIp);
                // stub no results
                will(returnValue(UpdateResultProto.UpdateResult
                        .newBuilder()
                        .setStatus(

UpdateResultProto.UpdateResult.ResponseStatus.NO_RESULTS_INCLUDED)
                        .build()));
            }
        });

        byte[] pingMessage = new byte[] { ... <bytes> ... };
        triggerMessageEvent(pipeline,
ChannelBuffers.wrappedBuffer(pingMessage));
        waitForExecutorThread();
    }

    private void waitForExecutorThread() throws InterruptedException {
        // test will fail if expectations are evaluated before executor
thread
        // returns, therefore we just sleep for a moment here.
        Thread.sleep(50);
    }

    private void triggerMessageEvent(
            ChannelPipeline cp, ChannelBuffer cb) throws Exception {
        UpstreamMessageEvent upMessage = new
UpstreamMessageEvent(mockChannel, cb, null);
        ChannelHandlerContext decoderCtx = cp.getContext("decoder");
        PacketDecoder decoder = (PacketDecoder) decoderCtx.getHandler();
        decoder.handleUpstream(decoderCtx, upMessage);
    }
}


A stubbed pipeline factory is created with handlers that have mocked
dependencies. triggerMessageEvent() starts the test by
calling handleUpstream() on my decoder. Then the pipeline interactions are
verified by checking the expectations on the mocks. Using something like
Mockito instead of JMock could really help with the verbosity of the
expectations in this case, but it was interesting to see everything that was
happening.

It was kind of a trial an error process setting these up (especially the
pipeline test), and it's pretty messy, but it might help you get started in
the direction you're going.

Jesse
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.jboss.org/pipermail/netty-users/attachments/20110510/ac81f677/attachment-0001.html 


More information about the netty-users mailing list