Netty4實戰第十章:Netty應用的單元測試
本章主要內容:
- 單元測試
- EmbeddedChannel
還好Netty提供了兩個類幫助開發者測試ChannelHandler。學習完這一章,就能學會如何測試你的專案,提高應用的健壯性。 本章使用的單元測試框架是JUnit 4,這可是個神器,搞Java的應該都知道的吧。它雖然簡單,但是功能很強大。經過前面的學習,大家應該可以實現自己的ChannelHandler了,這一章我們主要學習如何測試它。
一、前言
我們知道Netty的ChannelHandler總共有兩大類,一類處理收到的資料,一類處理髮送的資料,通過ChannelPipeline可以很容易的將這兩大類ChannelHandler結合在一起。ChannelHandler算是使用了常用設計模式中的責任鏈模式,可以很方便的複用裡面的實現。ChannelHandler只處理事件,邏輯很簡單,也很清晰,並且方便進行測試。
測試ChannelHandler一般建議使用嵌入式傳輸方式,前面說過,這種傳輸方式不會真正走網路,但是很容易傳輸事件,從而測試你的ChannelHandler實現。這種傳輸方式提供了一個特殊的Channel實現,名字叫EmbeddedChannel。
但是它是如何工作的呢?很簡單,可以向EmbeddedChannel寫資料模擬收到資料或傳送資料,然後檢查是否經過ChannelPipeline。這樣就可以檢查資料是否被編碼解碼或者觸發了什麼事件。
下表列出了AbstractEmbeddedChannel提供的常用方法。
名稱 |
描述 |
writeInbound(…) |
向Channel寫入資料,模擬Channel收到資料,也就 |
readInbound(…) |
從EmbeddedChannel中讀資料,返回經過ChannelPipeline中的 |
writeOutbound(…) |
向Channel寫入資料,模擬Channel傳送資料,也就 |
readOutbound(…) |
從EmbeddedChannel中讀資料,返回經過ChannelPipeline中的 |
finish() |
結束EmbeddedChannel,如果裡面有任何型別的可讀資料都會返回true,它也會呼叫Channel的close方法 |
為了更加清楚瞭解這些方法讀寫的資料在EmbeddedChannel中的流程,請看下面的結構圖。
從上圖中可以很明顯看出來,writeOutbound(…)會將資料寫到Channel並經過OutboundHandler,然後通過readOutbound(…)方法就能讀取到處理後的資料。模擬收到資料也是類似的,通過writeInbound(…)和readInbound(…)方法。收到的資料和傳送的資料的邏輯基本是一樣的,經過ChannelPipeline後到達終點後會儲存在EmbeddedChannel中。
瞭解了EmbeddedChannel大致結構,下面我們來學習下如何使用它來測試你的ChannelHandler。
二、測試ChannelHandler
為了測試ChannelHandler,最好還是使用EmbeddedChannel。下面會簡單介紹幾個例子,講述如何使用EmbeddedChannel測試我們編寫的ChannelHandler。
2.1、測試InboundHandler
我們先實現一個簡單的ByteToMessageDecoder,主要邏輯就是每次收到資料後將資料分成數量固定的組,如果資料不夠就不分然後等到下次收到資料的時候再檢查是否資料足夠,如下圖所示。
從上圖可以看出來,主要邏輯很簡單,就是讀取指定數量資料然後分組,完整程式碼如下。
import java.util.List;
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);
}
}
}
一般來說,我們實現了一段主要的邏輯,最好就使用單元測試驗證一下。即使你能保證你現在寫的程式碼沒什麼問題,但你後面可能會重構你的程式碼,所以寫一段單元測試,重構之後只要再跑一下單元測試,就能檢查重構是否出現問題。單元測試可以幫助開發者發現很多問題,預防在生產環境出現重大問題。
下面我們來測試一下剛才我們實現的解碼器。
package com.nan.netty.test;
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));
//驗證寫資料返回True
Assert.assertTrue(channel.writeInbound(input.readBytes(9)));
Assert.assertTrue(channel.finish());
//每次讀3個數據
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()方法主要邏輯是一個包含9個位元組的ByteBuf,警告我們實現的解碼器,變成了3個ByteBuf,每個ByteBuf包含3個位元組。通過writeInbound(…)方法將9個位元組寫到EmbeddedByteChannel中,呼叫finish()方法標記EmbeddedByteChannel已經結束,然後使用readInbound()方法讀出EmbeddedByteChannel中已經解碼完成的資料。
testFramesDecoded2()方法的大致邏輯和前一個方法一樣,唯一的區別就是寫入資料的時候先寫2個位元組,因此導致我們實現的FixedLengthFrameDecoder沒有解碼輸出,所以返回結果為false。
2.2、測試OutboundHandler
上面我們學習瞭如何測試InboundHandler,接下來我們學習如何單元測試OutboundHandler。測試OutboundHandler和測試InboundHandler的結構大致是一樣的,我們還是通過實際例子學習。
我們實現一個AbsIntegerEncoder,將負數變成正數,然後傳遞給下一個ChannelHandler,主要步驟如下:
- 收到ByteBuf後,呼叫Math.abs(..)將它們都轉成正數
- 轉換完成之後將資料傳遞給下一個ChannelHandler
AbsIntegerEncoder的程式碼如下。
package com.nan.netty.test;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import java.util.List;
public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext,
ByteBuf in, List<Object> out) throws Exception {
while (in.readableBytes() >= 4) {
int value = Math.abs(in.readInt());
out.add(value);
}
}
}
可以看出,實現很簡單,有整數就讀出來,取得絕對值,放入到結果列表中。這麼簡單的邏輯大家可能會覺得肯定不會出錯,不過有沒有錯誤最好還是通過單元測試來證明。
package com.nan.netty.test;
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() {
ByteBuf buf = Unpooled.buffer();
for (int i = 1; i < 10; i++) {
buf.writeInt(i * -1);
}
EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());
//模擬傳送資料
Assert.assertTrue(channel.writeOutbound(buf));
Assert.assertTrue(channel.finish());
//檢查是否是正數
for (int i = 1; i < 10; i++) {
Assert.assertEquals(i, (int) channel.readOutbound());
}
//沒有資料返回null
Assert.assertNull(channel.readOutbound());
}
}
三、測試捕獲異常
有些情況如果收到或傳送的資料有問題如資料量不足,這種情況可能需要丟擲一個異常。現在我們來測試一個丟擲異常的情況,實現一個解碼器,主要邏輯就是收到的資料長度超過限制時,就丟擲異常TooLongFrameException。收到資料超過指定長度時就清除這些資料並丟擲TooLongFrameException異常,然後下一個ChannelHandler通過exceptionCaught(…)方法就可以捕獲這個異常或者忽略。
package com.nan.netty.test;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.TooLongFrameException;
import java.util.List;
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 buf = in.readBytes(readableBytes);
out.add(buf);
}
}
單元測試還是選擇EmbeddedChannel最好,程式碼如下。
package com.nan.netty.test;
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 buf = Unpooled.buffer();
for (int i = 0; i < 9; i++) {
buf.writeByte(i);
}
ByteBuf input = buf.duplicate();
EmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3));
Assert.assertTrue(channel.writeInbound(input.readBytes(2)));
try {
channel.writeInbound(input.readBytes(4));
Assert.fail();
} catch (TooLongFrameException e) {
System.out.println("Catch TooLongFrameException");
}
Assert.assertTrue(channel.writeInbound(input.readBytes(3)));
Assert.assertTrue(channel.finish());
Assert.assertEquals(buf.readBytes(2), channel.readInbound());
Assert.assertEquals(buf.skipBytes(4).readBytes(3), channel.readInbound());
}
}
這個單元測試可能和前面的看著很像,但是有很明顯的區別,就是這裡使用了try
/ catch捕獲了異常,可以看到,使用EmbeddedChannel也可以測試這種特殊的需求。例子中雖然測試的是ByteToMessageDecoder,但是很明顯,任何ChannelHandler丟擲異常的情況都可以使用EmbeddedChannel進行測試。
四、總結
這一章我們主要學習了單元測試我們實現的ChannelHandler,單元測試框架使用的還是經典的JUnit。測試中使用的Channel型別是EmbeddedChannel,雖然它的實現很簡單,但功能是很完善的,可以幫助我們測試自己的ChannelHandler。下一章我們開始學習在實際專案中使用Netty,當然後面的應用可能沒有單元測試,但是請大家記住,在實際工作中你編寫的程式碼應該是有單元測試的,它的重要性不亞於你的業務程式碼。