Netty實戰九之單元測試

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中讀出來。

Netty實戰九之單元測試

2、使用EmbeddedChannel測試ChannelHandler

JUnit斷言

org.junit.Assert 類提供了很多用於測試的靜態方法。失敗的斷言將導致一個異常被拋出,並將終止當前正在執行中的測試。導入這些斷言的最高效的方式是通過一個import static語句來實現:

import static org.junit.Assert.*;

一旦這樣做了,就可以直接調用Assert方法了:

assertEquals(buf.readSlice(3),read);

3、測試入站消息

下圖展示了一個簡單的ByteToMessageDecoder實現。給定足夠的數據,這個實現將產生固定大小的幀。如果沒有足夠的數據可供讀取,它將等待下一個數據塊的到來,並將再次檢查是否產生一個新的幀。

Netty實戰九之單元測試
可以從圖中右側的幀看到的那樣,這個特定的解碼器將產生固定爲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中。
Netty實戰九之單元測試
以下代碼實現了這個邏輯。


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()方法中處理該異常或者忽略它。
Netty實戰九之單元測試
其實現如下代碼所示。


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是否在處理數據的過程中已經被處理了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章