本章介紹java
單元測試ide
EmbeddedChannel單元測試
會了使用一個或多個ChannelHandler處理接收/發送數據消息,可是如何測試它們呢?Netty提供了2個額外的類使得測試ChannelHandler變得很容易,本章講解如何測試Netty程序。測試使用JUnit4,若是不會用能夠慢慢了解。JUnit4很簡單,可是功能很強大。本章將重點講解測試已實現的ChannelHandler和編解碼器。測試
10.1 Generalthis
Netty提供了一個簡單的方法在ChannelPipeline上「堆疊」不一樣的ChannelHandler實現。全部的ChannelHandler都會參與處理事件,這個設計容許獨立出可重用的小邏輯塊,它只處理一個任務。這不只使代碼更清晰,也更容易測試。編碼
測試ChannelHandler能夠經過使用「嵌入式」傳輸很容易的傳遞事件槽管道以測試你的實現。.net
Netty提供了一個特定的Channel實現:EmbeddedChannel。設計
它是如何工做的呢?EmbeddedChannel的工做很是簡單,它容許寫入入站或出站數據,而後檢查ChannelPipeline的結束。這容許你檢查消息編碼/解碼或觸發ChannelHandler任何行爲。netty
編寫入站和出站的卻別是什麼?code
入站數據是經過ChannelInboundHandler處理,表明從遠程對等通道讀取數據;出站數據是經過ChannelOutboundHandler處理,表明寫入數據到遠程對等通道。所以測試ChannelHandler就會選擇writeInbound(...)或writeOutbound()(或者都選擇)。
EmbeddedChannel提供了下面一些方法:
writeInbound(Object...),寫一個消息到入站通道
writeOutbound(Object...),寫消息到出站通道
readInbound(),從EmbeddedChannel讀取入站消息,可能返回null
readOutbound(),從EmbeddedChannel讀取出站消息,可能返回null
finish(),標示EmbeddedChannel已結束,任何寫數據都會失敗
爲了更清楚的瞭解其處理過程,看下圖:
如上圖所示,使用writeOutbound(...)寫消息到通道,消息在出站方法經過ChannelPipeline,以後就可使用readOutbound()讀取消息。着一樣使用與入站,使用writeInbound(...)和readInbound()。處理入站和出站是類似的,它老是遍歷整個ChannelPipeline直到ChannelPipeline結束,並將處理過的消息存儲在EmbeddedChannel中。
10.2 測試ChannelHandler
測試ChannelHandler最好的選擇是使用EmbeddedChannel。
10.2.1 測試處理入站消息的handler
咱們來編寫一個簡單的ByteToMessageDecoder實現,有足夠的數據能夠讀取時將產生固定大小的包,若是沒有足夠的數據能夠讀取,則會等待下一個數據塊並再次檢查是否能夠產生一個完整包。下圖顯示了從新組裝接收的字節:
如上圖所示,它可能會佔用一個以上的「event」以獲取足夠的字節產生一個數據包,並將它傳遞到ChannelPipeline中的下一個ChannelHandler,看下面代碼:
package netty.in.action; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; public class FixedLengthFrameDecoder extends ByteToMessageDecoder { 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 { while (in.readableBytes() >= frameLength) { ByteBuf buf = in.readBytes(frameLength); out.add(buf); } } }
解碼器的實現完成了,寫一個單元測試的方法是個好主意。即便代碼看起來沒啥問題,可是也應該進行單元測試,這樣能在部署到生產以前就發現問題。如今讓咱們來看看如何使用EmbeddedChannel來完成測試,看下面代碼:
package netty.in.action; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import org.junit.Assert; import org.junit.Test; public class FixedLengthFrameDecoderTest { @Test public void testFramesDecoded() { 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 bytes Assert.assertTrue(channel.writeInbound(input)); Assert.assertTrue(channel.finish()); // read message Assert.assertEquals(buf.readBytes(3), channel.readInbound()); Assert.assertEquals(buf.readBytes(3), channel.readInbound()); Assert.assertEquals(buf.readBytes(3), channel.readInbound()); Assert.assertNull(channel.readInbound()); } @Test public void testFramesDecoded2() { 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)); Assert.assertFalse(channel.writeInbound(input.readBytes(2))); Assert.assertTrue(channel.writeInbound(input.readBytes(7))); Assert.assertTrue(channel.finish()); Assert.assertEquals(buf.readBytes(3), channel.readInbound()); Assert.assertEquals(buf.readBytes(3), channel.readInbound()); Assert.assertEquals(buf.readBytes(3), channel.readInbound()); Assert.assertNull(channel.readInbound()); } }
如上面代碼,testFramesDecoded()方法想測試一個ByteBuf,這個ByteBuf包含9個可讀字節,被解碼成包含了3個可讀字節的ByteBuf。
你可能注意到,它寫入9字節到通道是經過調用writeInbound()方法,以後再執行finish()來將EmbeddedChannel標記爲已完成,最後調用readInbound()方法來獲取EmbeddedChannel中的數據,直到沒有可讀字節。
testFramesDecoded2()方法採起一樣的方式,但有一個區別就是入站ByteBuf分兩步寫的,當調用writeInbound(input.readBytes(2))後返回false時,FixedLengthFrameDecoder值會產生輸出,至少有3個字節是可讀,testFramesDecoded2()測試的工做至關於testFramesDecoded()。
10.2.2 測試處理出站消息的handler
測試處理出站消息和測試處理入站消息不太同樣,例若有一個繼承MessageToMessageEncoder的AbsIntegerEncoder類,它所作的事情以下:
將已接收的數據flush()後將從ByteBuf讀取全部整數並調用Math.abs(...)
完成後將字節寫入ChannelPipeline中下一個ChannelHandler的ByteBuf中
看下圖處理過程:
看下面代碼:
package netty.in.action; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageEncoder; public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> { @Override protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception { while(msg.readableBytes() >= 4){ int value = Math.abs(msg.readInt()); out.add(value); } } }
下面代碼是測試AbsIntegerEncoder:
package netty.in.action; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import org.junit.Assert; import org.junit.Test; public class AbsIntegerEncoderTest { @Test public void testEncoded() { //建立一個能容納10個int的ByteBuf ByteBuf buf = Unpooled.buffer(); for (int i = 1; i < 10; i++) { buf.writeInt(i * -1); } //建立EmbeddedChannel對象 EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder()); //將buf數據寫入出站EmbeddedChannel Assert.assertTrue(channel.writeOutbound(buf)); //標示EmbeddedChannel完成 Assert.assertTrue(channel.finish()); //讀取出站數據 ByteBuf output = (ByteBuf) channel.readOutbound(); for (int i = 1; i < 10; i++) { Assert.assertEquals(i, output.readInt()); } Assert.assertFalse(output.isReadable()); Assert.assertNull(channel.readOutbound()); } }
10.3 測試異常處理
有時候傳輸的入站或出站數據不夠,一般這種狀況也須要處理,例如拋出一個異常。這多是你錯誤的輸入或處理大的資源或其餘的異常致使。咱們來寫一個實現,若是輸入字節超出限制長度就拋出TooLongFrameException,這樣的功能通常用來防止資源耗盡。看下圖:
上圖顯示幀的大小被限制爲3字節,若輸入的字節超過3字節,則超過的字節被丟棄並拋出TooLongFrameException。在ChannelPipeline中的其餘ChannelHandler實現能夠處理TooLongFrameException或者忽略異常。處理異常在ChannelHandler.exceptionCaught()方法中完成,ChannelHandler提供了一些具體的實現,看下面代碼:
package netty.in.action; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.TooLongFrameException; 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) { in.clear(); throw new TooLongFrameException(); } // 讀取ByteBuf並放到List中 ByteBuf buf = in.readBytes(readableBytes); out.add(buf); } }
測試FrameChunkDecoder的代碼以下:
package netty.in.action; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.TooLongFrameException; import org.junit.Assert; import org.junit.Test; public class FrameChunkDecoderTest { @Test public void testFramesDecoded() { //建立ByteBuf並填充9字節數據 ByteBuf buf = Unpooled.buffer(); for (int i = 0; i < 9; i++) { buf.writeByte(i); } //複製一個ByteBuf ByteBuf input = buf.duplicate(); //建立EmbeddedChannel EmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3)); //讀取2個字節寫入入站通道 Assert.assertTrue(channel.writeInbound(input.readBytes(2))); try { //讀取4個字節寫入入站通道 channel.writeInbound(input.readBytes(4)); Assert.fail(); } catch (TooLongFrameException e) { } //讀取3個字節寫入入站通道 Assert.assertTrue(channel.writeInbound(input.readBytes(3))); //標識完成 Assert.assertTrue(channel.finish()); //從EmbeddedChannel入去入站數據 Assert.assertEquals(buf.readBytes(2), channel.readInbound()); Assert.assertEquals(buf.skipBytes(4).readBytes(3), channel.readInbound()); } }