EmbeddedChannel overview

Channelhandlers are key elements of Netty programs, so testing them thoroughly should be a standard part of your development process. Embedded Channels are provided by Netty specifically to improve unit testing for ChannelHandlers. Netty provides what it calls Embedded transport, a function of Embedded channel that provides an easy way to propagate events through channel pipelines

The method is to write either inbound or outbound data to the EmbeddedChannel and then check to see if anything reaches the end of the CHannelPipeline. This way you can determine if the message has been encoded or decoded and if any ChannelHandler actions have been triggered

The following table lists the related methods for EmbeddedChannel

Inbound data is processed by ChannelInboundHandler and represents data read from the remote node. Outbound data is handled by the ChannelOutboundHandler and represents data to be written to the remote node. Depending on the ChannelHandler you are testing, you can use either the Inbound() or Outbound() method pairs, or both

The following figure shows how data flows through the ChannelPipeline using an EmbeddedChannel approach. You can use the writeOutbound() method to write messages to a Channel and route them through the Channel pipeline in the direction of the outbound. You can then use the readOutbound() method to read the processed messages to ensure that the results are as expected. Similarly, for inbound data, you need to use the writeInbound() and readInbound() methods! [](G:\SSS\Java\Java SE\ blog \Netty\EmbeddedChannel data stream.png)

Test the ChannelHandler using EmbeddedChannel

1. Test the inbound message

The following code shows a simple ByteToMessageDecoder implementation that, given enough data, will produce frames of a fixed size. If there is not enough data to read, it waits for the next block of data and checks again to see if it can produce a new frame

public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
    // Specify the length of the frame to be generated
    private final int frameLength;

    public FixedLengthFrameDecoder(int frameLength) {
        if (frameLength <= 0) {
            throw new IllegalArgumentException("frameLength must be a positive integer:" + frameLength);
        }
        this.frameLength = frameLength;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // Check if there are enough bytes to be read to generate the next frame
        while (in.readableBytes() >= frameLength) {
            // Read a new frame from ByteBuf
            ByteBuf buf = in.readBytes(frameLength);
            // Add this frame to the list of decoded messagesout.add(buf); }}}Copy the code

The following code shows a test of the previous code using EmbeddedChannel

public class FixedLengthFrameDecoderTest {
    
    @Test
    public void testFrameDecoded(a) {
        // Create a ByteBuf and store 9 bytes
        ByteBuf buf = Unpooled.buffer();
        for (int i = 0; i < 9; i++) {
            buf.writeByte(i);
        }
        ByteBuf input = buf.duplicate();
        EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));
        // write data to the EmbeddedChannel
        System.out.println(channel.writeInbound(input.retain()));//true
        // mark Channel as completed
        System.out.println(channel.finish());//true

        // Reads the generated message and verifies that there are 3 frames, each of which is 3 bytes
        ByteBuf read = channel.readInbound();
        System.out.println(buf.readSlice(3).equals(read));// true

        read = channel.readInbound();
        System.out.println(buf.readSlice(3).equals(read));// true
        read.release();

        read = channel.readInbound();
        System.out.println(buf.readSlice(3).equals(read));// true
        read.release();

        System.out.println(channel.readInbound() == null);// true
        buf.release();
    }

    @Test
    public void testFramesDescode2(a) {
        ByteBuf buf = Unpooled.buffer();
        for (int i = 0; i < 9; i++) {
            buf.writeByte(i);
        }
        ByteBuf input = buf.duplicate();
        EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));
        // Return false because there is no complete frame available to read
        System.out.println(channel.writeInbound(input.readBytes(2)));// false
        System.out.println(channel.writeInbound(input.readBytes(7)));// true

        System.out.println(channel.finish());// true
        ByteBuf read = channel.readInbound();
        System.out.println(buf.readSlice(3) == read);// false
        read.release();

        read = channel.readInbound();
        System.out.println(buf.readSlice(3) == read);// false
        read.release();

        read = channel.readInbound();
        System.out.println(buf.readSlice(3) == read);// false
        read.release();

        System.out.println(channel.readInbound() == null);// truebuf.release(); }}Copy the code

2. Test the inbound message

Testing the processing of the outbound message is similar to what you’ve just seen. In the following example, we’ll show how to use the EmbeddedChannel to test another ChannelOutboundHandler in the form of an encoder. Encoder is a component that converts one message format to another

The example will work as follows:

  • An EmbeddedChannel that holds the AbsIntegerEncoder will write outbound data as a negative integer of 4 bytes
  • The encoder will read each negative integer from the passed ByteBuf and will call math.abs () to get its absolute value
  • The encoder will write the absolute value of each negative integer to the ChannelPipeline

The following code shows this logic

public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {
    @Override
    protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        while (msg.readableBytes() >= 4) {
            // Reads the next integer from the input ByteBuf and computes its absolute value
            int value = Math.abs(msg.readInt());
            // Write the integer to the List of encoded messagesout.add(value); }}}Copy the code

Use the EmbeddedChannel to test the code

public class AbsIntegerEncoderTest {
    @Test
    public void testEncoded(a) {
        ByteBuf buf = Unpooled.buffer();
        for (int i = 1; i < 10; i++) {
            buf.writeInt(i * -1);
        }
        // Create an EmbeddedChanel and install an AbsIntegerEncoder to test
        EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());
        // Write ByteBuf and call readOutbound() to generate data
        System.out.println(channel.writeOutbound(buf));
        System.out.println(channel.finish());

        channel.readOutbound();
        for (int i = 1; i < 10; i++) {
            int temp = channel.readOutbound();
            System.out.println(temp);
        }
        System.out.println(channel.readOutbound() == null); }}Copy the code

Here are the steps performed in the code.

  • Writes a 4-byte negative integer to a new ByteBuf
  • Create an EmbeddedChannel and assign it an AbsIntegerEncoder
  • Call the writeOutbound() method on the EmbeddedChannel to write the ByteBuf
  • Marks the Channel as completed
  • Read all integers from the outbound side of the EmbeddedChannel and verify that only absolute values are generated

Test Exception Handling

Applications often need to perform more complex tasks than transforming data. For example, you may need to deal with malformed inputs or excessive data. In the next example, if the number of bytes read exceeds a certain limit, we will throw a TooLongFrameException, a method often used to guard against resource exhaustion

The code is as follows

// Extend ByteToMessageDecoder to convert inbound bytecode to messages
public class FrameChunkDecoder extends ByteToMessageDecoder {
    
    private final int maxFrameSize;

    public FrameChunkDecoder(int maxFrameSize) {
        this.maxFrameSize = maxFrameSize;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int readableBytes = in.readableBytes();
        if (readableBytes > maxFrameSize) {
            // If the frame is too large, discard it and throw a TooLongFrameException
            in.clear();
            throw new TooLongFrameException();
        }
        // Otherwise, read a new frame from ByteBuf
        ByteBuf buf = in.readBytes(readableBytes);
        // This frame is added to the List of decoded messagesout.add(buf); }}Copy the code

Use the EmbeddedChannel to test the code

public class FrameChunkDecoderTest {
    @Test
    public void testFramesDecoded(a) {
        ByteBuf buf = Unpooled.buffer();
        for (int i = 0; i < 9; i++) {
            buf.writeByte(i);
        }
        ByteBuf input = buf.duplicate();

        // Create an EmbeddedChannel and install a FixedLengthFrameDecoder of 3 bytes to it
        EmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3));

        System.out.println(channel.writeInbound(input.readBytes(2)));
        try {
            // Write a frame of 4 bytes and catch the expected exception
            channel.writeInbound(input.readBytes(4));
        } catch (TooLongFrameException e) {
            e.printStackTrace();
        }

        // Write the remaining 2 bytes to produce a valid frame
        System.out.println(channel.writeInbound(input.readBytes(3)));//true
        System.out.println(channel.finish());

        // Read the resulting message and validate the value
        ByteBuf read = channel.readInbound();
        System.out.println(read.equals(buf.readSlice(2)));//true
        read.release();

        read = channel.readInbound();
        System.out.println(read.equals(buf.skipBytes(4).readSlice(3)));//trueread.release(); buf.release(); }}Copy the code