(譯)Netty In Action第九章—單元測試
這章涵蓋一下內容
- 單元測試
- EmbeddedChannel概覽
- 使用EmbeddedChannel測試ChannelHandlers
ChannelHandlers是Netty應用程式的重要因素,所以徹底測試它們應該是開發過程的標準部分。最佳實踐要求你進行測試不僅要證明你的實現是正確的,而且容易隔離因程式碼修改而突然出現的問題。這類測試叫做單元測試。
雖然單元測試沒有通用的定義,但是大部分實踐者同意基本原則。基本思想是在一個儘可能小的組塊中測試你的程式碼,儘可能隔離其他程式碼模組和執行時依賴,比如資料庫和網路。如果你可以通過測試每個單元獨自正確工作來驗證,當代碼出現錯誤時,你會很容易找到罪魁禍首。
在這一章我們將學習一個特別的Channel實現——EmbeddedChannel,它是Netty特意提供來促進ChannelHandlers的單元測試。
因為被測試的程式碼模組或者單元將在正式執行時環境之外執行,你需要一個框架或者工具來執行它。在我們的例子中,我們將使用JUnit 4作為我們的測試框架,所以你需要對它的用法有一個基本的認知。如果你沒聽說過它,不需要擔心;雖然它強大但也簡單,在JUnit官網(www.junit.org)上你可以查閱所有你需要的資料。
你會發現回顧之前關於ChannelHandler和編碼解碼器的章節是有用的,因為這些將為我們的例子提供素材。
9.1 EmbeddedChannel概覽
你已經知道ChannelHandler實現可以在一個ChannelPipeline中束縛一起來建立你的應用程式的業務邏輯。我們之前解釋了這種設計支援將潛在複雜的流程分解成小的且可重用的元件,每一個元件處理一個明確定義的任務或步驟。在這一章我們將給你展示它是如何簡化測試的。
Netty提供嵌入式傳輸來測試ChannelHandlers。此傳輸是一種特別的Channel實現——EmbeddedChannel的功能,它提供了一個簡單的方法來通過管道傳輸事件。
這個想法簡單明瞭:你寫入站和出站資料到EmbeddedChannel,然後檢查是否有到達ChannelPipeline末端的資料。用這種方法你可以決定訊息是否被編碼或解碼和任一ChannelHandler操作是否被觸發。
EmbeddedChannel相關的方法如表9.1所列。
表 9.1 特別的EmbeddedChannel方法
方法 | 作用 |
---|---|
writeInbound(Object… msgs) | 寫入站資料到EmbeddedChannel。如果資料可以通過readInbound()從EmbeddedChannel讀取返回true。 |
readInbound() | 從EmbeddedChannel讀入站資料。返回的任何內容都遍歷整個ChannelPipeline。如果沒有準備好的資料來讀取,返回null。 |
writeOutbound(Object… msgs) | 寫出站資料到EmbeddedChannel。如果目前可以通過readOutbound()從EmbeddedChannel讀取資料,返回true。 |
readOutbound() | 從EmbeddedChannel讀出站資料。返回的任何內容都遍歷整個ChannelPipeline。如果沒有準備好的資料來讀取,返回null。 |
finish() | 如果入站或出站資料可讀,標記EmbeddedChannel為完成狀態並返回true。這也會呼叫EmbeddedChannel的close()。 |
圖 9.1 EmbeddedChannel資料流
入站資料被ChannelInboundHandlers處理並展示從遠端讀取的資料。出站資料被ChannelOutboundHandlers處理並展示帶寫入遠端的資料。根據你正在測試的ChannelHandler,你將使用*Inbound() 或*Outbound()方法對,也有可能兩者都用。
圖9.1展示了資料使用EmbeddedChannel的方法如何流經ChannelPipeline。你可以使用 writeOutbound()寫訊息到Channel並通過ChannelPipeline將它沿著出站方向傳遞。隨後你可以使用readoutbound()讀取處理過的訊息,以此判斷結果是否和你期待的一樣。相似地,對於入站資料你使用writeInbound()和readInbound()。
在每種情況中,訊息都是通過ChannelPipeline傳遞並被相關的ChannelInboundHandlers和ChannelOutboundHandlers處理。如果訊息未被消費,你可以在處理他們之後,合理使用 readInbound()或readOutbound()讀取Channel之外的訊息。
讓我們進一步瞭解這兩種場景(圖9.1入站和出站場景),並檢視他們如何應用到測試你的應用程式邏輯。
9.2 使用EmbeddedChannel測試ChannelHandlers
在這一節我們將闡釋如何用EmbeddedChannel測試ChannelHandler。
JUnit斷言
在測試中類org.junit.Assert提供了很多靜態方法以供使用。一個失敗的斷言將造成異常被丟擲並會終止當前正在執行的測試。引入這些斷言最有效的方法是通過一個引入靜態宣告:
import static org.junit.Assert.*;
一旦你已經引入此宣告,你可以直接呼叫斷言方法:
assertEquals(buf.readSlice(3), read);
9.2.1 測試入站資訊
圖9.2展示了一個簡單的ByteToMessageDecoder實現。給定足夠的資料,這將產生固定大小的幀。如果沒有足夠準備好的資料來讀取,它將等待下一個資料塊並再次檢查幀是否可以被建立。
圖 9.2 通過FixedLengthFrameDecoder解碼
正如你可以從圖右側看到的幀,這種特別的解碼器生成了固定大小為3個位元組的幀。因此它需要多個事件來提供足夠的位元組以此生成一個幀。
最後,每一幀將傳遞到ChannelPipeline的下一個ChannelHandler。
解碼器的實現如下程式碼清單所示。
碼單 9.1 FixedLengthFrameDecoder
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 {
//Checks if enough bytes can be read to produce the next frame
while (in.readableBytes() >= frameLength) {
ByteBuf buf = in.readBytes(frameLength);
out.add(buf);
}
}
}
現在讓我們建立一個單元測試,以確保此程式碼按預期工作。正如我們之前提出的,及時在簡單的程式碼中,單元測試有助於阻止將來程式碼重構可能發生的問題和診斷重構程式碼。
以下程式碼清單展示了一個使用EmbeddedChannel對前述程式碼的測試。
碼單 9.2 測試FixedLengthFrameDecoder
public class FixedLengthFrameDecoderTest extends TestCase {
@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
assertTrue(channel.writeInbound(input.retain()));
assertTrue(channel.finish());
// read messages
ByteBuf read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
assertNull(channel.readInbound());
buf.release();
}
@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));
assertFalse(channel.writeInbound(input.readBytes(2)));
assertTrue(channel.writeInbound(input.readBytes(7)));
assertTrue(channel.finish());
ByteBuf read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(3), read);
read.release();
assertNull(channel.readInbound());
buf.release();
}
}
方法testFramesDecoded()驗證一個包含9個可讀位元組的ByteBuf被解碼成3個包含3位元組的ByteBufs。注意ByteBuf是如何在一次呼叫writeInbound()中填充9個可讀位元組。然後,執行finish()來標識EmbeddedChannel完成。最後,呼叫readInbound()來從EmbeddedChannel中精確地讀取3幀和一個null。
方法testFramesDecoded2()是相似的,有一點不同:入站ByteBufs用兩步寫入。當呼叫writeInbound(input.readBytes(2))時,返回false.為什麼呢?如表9.1所述,如果隨後呼叫readInbound()會返回資料則writeInbound()返回true。但是FixedLengthFrameDecoder當且僅當3個或者更多位元組可讀時才會產生輸出。其餘測試和testFramesDecoded()等價。
9.2.2 測試出站訊息
測試出站訊息過程和你剛剛在上一節看到的相似。我們將在下一個例子中展示如何使用EmbeddedChannel以編碼器形式來測試ChannelOutboundHandler,此編碼器元件將一種訊息格式轉換為另一種格式。下一章你將非常詳細地學習編碼器和解碼器,所以現在我們僅僅提及我們正在測試的攔截器——AbsIntegerEncoder,將負整數轉化為絕對值的Netty的MessageToMessageEncoder的特化。
例子將按如下步驟工作:
- 持有AbsIntegerEncoder的EmbeddedChannel將寫入4位元組負整數格式的出站資料。
- 解碼器將從寫入的ByteBuf讀取每一個負整數並呼叫Math.abs()得到其絕對值。
- 解碼器將把每一個整數的絕對值寫入ChannelHandlerPipeline。
圖9.3展示了邏輯。
圖 9.3 通過AbsIntegerEncoder解碼
以下程式碼清單實現圖9.3中闡明的邏輯。encode()方法將生產的值寫入一個List。
碼單 9.3 AbsIntegerEncoder
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);
}
}
}
以下程式碼清單使用EmbeddedChannel測試AbsIntegerEncoder程式碼。
碼單 9.4 測試AbsIntegerEncoder
public class AbsIntegerEncoderTest {
@Test
public void test() {
fail("Not yet implemented");
}
@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());
assertTrue(channel.writeOutbound(buf));
assertTrue(channel.finish());
// read bytes
for (int i = 1; i < 10; i++) {
assertEquals(i, channel.readOutbound());
}
assertNull(channel.readOutbound());
}
}
這裡是程式碼執行的步驟:
- 將負的4位元組整數寫入一個新的ByteBuf
- 建立一個EmbeddedChannel並分配一個AbsIntegerEncoder給它
- 在EmbeddedChannel上呼叫writeOutbound()以寫入ByteBuf
- 標識完成的通道
- 從EmbeddedChannel的入站端讀取所有整數並驗證僅僅生成絕對值。
9.3 測試異常處理
除了轉換資料之外,應用程式通常還要執行其他任務。例如,你需要處理格式錯誤的輸入或資料量過大。在下一個例子中,如果位元組讀取的數量超過指定的限制,我們將丟擲TooLongFrameException。這是一個經常用來防止資源耗盡的方案。
在圖9.4椎間盤美好最大幀大小已被設為3位元組。如果幀的大小超過這個限制,那麼它的位元組是廢棄的並且丟擲TooLongFrameException。管道中其他的ChannelHandlers可以在exceptionCaught()中處理異常或者忽視它。
圖 9.4 通過FrameChunkDecoder解碼
實現如下程式碼清單所示。
碼單 9.5 FrameChunkDecoder
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) {
// discard the bytes
in.clear();
throw new TooLongFrameException();
}
ByteBuf buf = in.readBytes(readableBytes);
out.add(buf);
}
}
我們再一次使用EmbeddedChannel測試上面的程式碼。
碼單 9.6 測試FrameChunkDecoder
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));
assertTrue(channel.writeInbound(input.readBytes(2)));
try {
channel.writeInbound(input.readBytes(4));
Assert.fail();
} catch (TooLongFrameException e) {
// expected exception
}
assertTrue(channel.writeInbound(input.readBytes(3)));
assertTrue(channel.finish());
// Read frames
ByteBuf read = (ByteBuf) channel.readInbound();
assertEquals(buf.readSlice(2), read);
read.release();
read = (ByteBuf) channel.readInbound();
assertEquals(buf.skipBytes(4).readSlice(3), read);
read.release();
buf.release();
}
}
這乍一看和碼單9.2中的測試相當相似,但它有一個有趣的轉折;也就是TooLongFrameException的處理。這裡使用的try/catch塊是EmbeddedChannel的一大特色。如果write*方法其中一個產生檢查的Exception,它將被包裝在RuntimeException中丟擲。這使得在處理資料期間測試一個Exception是否被處理變得簡單。
這裡闡明的測試方案可以與任何拋異常的ChannelHandler實現一起使用。
9.4 總結
使用JUnit等測試工具進行單元測試是保證程式碼正確性和增強其可維護性的極為有效的方法。在本章中,你學習瞭如何使用Netty提供的測試工具來測試自定義ChannelHandler。
在接下來的章節中,我們將專注於使用Netty編寫實際應用程式。我們不會再提供任何測試程式碼示例,因此我們希望你能牢記我們在此演示的測試方法的重要性。