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