ChannelHandler是Netty應用程序的關鍵元素,所以徹底地測試他們應該是你的開發過程的一個標準部分。最佳實踐要求你的測試不僅要能夠證明你的實現是正確的,而且還要能夠很容易地隔離那些因修改代碼而突然出現的問題。這種類型的測試叫做單元測試。
其基本思想是,以儘可能小的區塊測試你的代碼,並且儘可能地和其他的代碼模塊以及運行時的依賴相隔離。
1、EmbeddedChannel概述
你已經知道,可以將ChannelPipeline中的ChannelHandler實現連接在一起,以構建你的應用程序的業務邏輯。之前已經解釋過,這種設計支持將任何潛在的複雜處理過程分解爲小的可重用的組件,每個組件都將處理一個明確定義的任務或者步驟。
Netty提供了它所謂的Embedded傳輸,用於測試ChannelHandler。這個傳輸是一種特殊的Channel實現——EmbeddedChannel——的功能。這是實現提供了通過ChannelPipeline傳播事件的簡便方法。
這個想法是直截了當的:將入站數據或者出站數據寫入到EmbeddedChannel中,然後檢查是否有任何東西到達了ChannelPipeline的尾端。以這種方式,你便可以確定消息是否被編碼或者被解碼過了,以及是否觸發了任何的ChannelHandler動作。
下圖展示了使用EmbeddedChannel的方法,數據是如何流經ChannelPipeline的。你可以使用writeOutbound()方法將消息寫到Channel中,並通過ChannelPipeline沿着出站的方向傳遞。隨後,你可以使用readOutbound()方法來讀取已被處理過的消息,已確定結果是否和預期一樣。類似地,對於入站數據,你需要使用writeInbound()和readInbound()方法。
在每種情況下,消息都將會傳遞過ChannelPipeline,並且被相關的ChannelInboundHandler或者ChannelOutboundHandler處理。如果消息沒有被消費,那麼你可以使用readInbound()或者readOutbound()方法來在處理過了這些消息之後,酌情把它們從Channel中讀出來。
2、使用EmbeddedChannel測試ChannelHandler
JUnit斷言
org.junit.Assert 類提供了很多用於測試的靜態方法。失敗的斷言將導致一個異常被拋出,並將終止當前正在執行中的測試。導入這些斷言的最高效的方式是通過一個import static語句來實現:
import static org.junit.Assert.*;
一旦這樣做了,就可以直接調用Assert方法了:
assertEquals(buf.readSlice(3),read);
3、測試入站消息
下圖展示了一個簡單的ByteToMessageDecoder實現。給定足夠的數據,這個實現將產生固定大小的幀。如果沒有足夠的數據可供讀取,它將等待下一個數據塊的到來,並將再次檢查是否產生一個新的幀。
可以從圖中右側的幀看到的那樣,這個特定的解碼器將產生固定爲3字節大小的幀。因此,它可能會需要多個事件來提供足夠的字節數以產生一個幀。
最終,每個幀都會被傳遞給ChannelPipeline中的下一個ChannelHandler,該解碼器的實現如下代碼所示。
//擴展ByteToMessageDecoder以處理入站字節,並將它們解碼爲消息public class FixedLengthFrameDecoder extends ByteToMessageDecoder{
private final int frameLength; //指定要生成的幀的長度
public FixedLengthFrameDecoder(int frameLength) throws IllegalAccessException { if (frameLength <= 0){ throw new IllegalAccessException("frameLength must be a positive integer: " + frameLength);
} this.frameLength = frameLength;
} @Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { //檢查是否有足夠的字節可以被讀取,以生成下一個幀
while (byteBuf.readableBytes() >= frameLength){ //從ByteBuf中讀取一個新幀
ByteBuf buf = byteBuf.readBytes(frameLength); //將該幀添加到已被解碼的消息列表中
list.add(buf);
}
}
}
以下代碼展示了一個使用EmbeddedChannel的對於前面代碼的測試
public class FixedLengthFrameDecoderTest {
@Test public void decode() throws Exception { //創建一個ByteBuf,並存儲9個字節
ByteBuf buf = Unpooled.buffer(); for (int i = 0; i < 9; i++){
buf.writeByte(i);
}
ByteBuf input = buf.duplicate(); //創建一個EmbeddedChannel,並添加一個FixedLengthFrameDecoder,其將以3字節的幀長度被測試
EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3)); //write bytes
//將數據寫入EmbeddedChannel
assertTrue(channel.writeInbound(input.retain())); //標記Channel爲已完成狀態
assertTrue(channel.finish()); //read messages
//讀取所生成的消息,並且驗證是否有3幀,其中每幀都爲3字節
ByteBuf read = (ByteBuf)channel.readInbound();
assertEquals(buf.readSlice(3),read); read.release();
assertNull(channel.readInbound());
buf.release();
}
}
4、測試出站消息
簡單地提及我們正在測試的處理器——AbsIntegerEncoder,它是netty的MessageToMessageEncoder的一個特殊化的實現,用於將負值整數轉換爲絕對值。
該示例將會按照下列方式工作:
——持有AbsIntegerEncoder的EmbeddedChannel將會以4字節的負整數的形式寫出站數據。
——編碼器將從傳入的ByteBuf中讀取每個負整數,並將會調用Math.abs()方法來獲取其絕對值
——編碼器將會把每個負整數的絕對值寫到ChannelPipeline中。
以下代碼實現了這個邏輯。
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf>{
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception { //檢查是否有足夠的字節用來編碼
while (byteBuf.readableBytes() >= 4){ //從輸入的ByteBuf中讀取下一個整數,並且計算其絕對值
int value = Math.abs(byteBuf.readInt()); //將該整數寫入到編碼消息的List中
list.add(value);
}
}
}
以下代碼使用了EmbeddedChannel來測試代碼
public class AbsIntegerEncoderTest { @Test
public void encode() throws Exception { //創建一個ByteBuf,並且寫入9個負整數
ByteBuf buf = Unpooled.buffer(); for (int i = 1; i < 10; i++){
buf.writeInt(i * -1);
} //創建一個EmbeddedChannel,並安裝一個要測試的AbsIntegerEncoder
EmbeddedChannel channel = new EmbeddedChannel( new AbsIntegerEncoder()); //寫入ByteBuf,並斷言調用readOutbound()方法將會產生數據
assertTrue(channel.writeOutbound(buf));
assertTrue(channel.finish()); //讀取所產生的消息,並斷言它們包含了對應的絕對值
//read bytes
for (int i=1; i < 10; i++){
assertEquals(i , channel.readOutbound());
}
assertNull(channel.readOutbound());
}
}
5、測試異常處理
應用程序通常需要執行比轉換數據更加複雜的任務。例如,你可能需要處理格式不正確的輸入或者過量的數據。下一個示例中,如果所讀取的字節數超出了特定的限制,我們將會拋出一個TooLongFrameException。這是一種經常用來防範資源被耗盡的方法。
如下圖,最大的幀大小已經被設置爲3字節,如果一個幀的大小超過了該限制,那麼程序將會丟棄它的字節,並拋出一個TooLongFrameException。位於ChannelPipeline中的其它ChannelHandler可以選擇在exceptionCaught()方法中處理該異常或者忽略它。
其實現如下代碼所示。
public class FrameChunkDecoder extends ByteToMessageDecoder{
private final int maxFrameSize; public FrameChunkDecoder(int maxFrameSize) { this.maxFrameSize = maxFrameSize;
} @Override
protected void decode(ChannelHandlerContext channelHandlerContext,
ByteBuf in, List<Object> out) throws Exception { int readableBytes = in.readableBytes(); //如果該幀太大,則丟棄它並拋出異常
if (readableBytes > maxFrameSize){ //discard the bytes
in.clear(); throw new TooLongFrameException();
} //從ByteBuf中讀取一個新的幀
ByteBuf buf = in.readBytes(readableBytes); //將該幀添加到解碼消息的List中
out.add(buf);
}
}
使用的Try/Catch塊是EmbeddedChannel的一個特殊功能。如果其中一個write*方法產生了一個受檢查的Exception,那麼它將會被包裝在一個RuntimeException中並拋出,這使得可以容易地測試出一個Exception是否在處理數據的過程中已經被處理了。