Netty 框架學習 —— 單元測試

低吟不作語發表於2021-06-25

EmbeddedChannel 概述

ChannelHandler 是 Netty 程式的關鍵元素,所以徹底地測試它們應該是你的開發過程中的一個標準部分,EmbeddedChannel 是 Netty 專門為改進針對 ChannelHandler 的單元測試而提供的。Netty 提供了它所謂的 Embedded 傳輸,這個傳輸是 EmbeddedChannel 的功能,提供了通過 ChannelPipeline 傳播事件的簡便方法

這個方法是:將入站資料或者出站資料寫入到 EmbeddedChannel 中,然後檢查是否有任何東西到達 CHannelPipeline 的尾端。通過這種方式,你可以確定訊息是否已經被編碼或者解碼過,以及是否觸發了任何 ChannelHandler 動作

下表列出了 EmbeddedChannel 的相關方法

入站資料由 ChannelInboundHandler 處理,代表從遠端節點讀取的資料。出站資料由 ChannelOutboundHandler 處理,代表將要寫到遠端節點的資料。根據你要測試的 ChannelHandler,你可以使用 Inbound() 或者 Outbound() 方法對,或者兼而有之

下圖展示了使用 EmbeddedChannel 的方法,資料是如何流經 ChannelPipeline 的。 你可以使用 writeOutbound()方法將訊息寫到 Channel 中,並通過 ChannelPipeline 沿 著出站的方向傳遞。隨後,你可以使用 readOutbound()方法來讀取已被處理過的訊息,以確 定結果是否和預期一樣。類似地,對於入站資料,你需要使用 writeInbound()和 readInbound() 方法
![](G:\SSS\Java\Java SE\部落格\Netty\EmbeddedChannel 的資料流.png)


使用 EmbeddedChannel 測試 ChannelHandler

1. 測試入站訊息

下述程式碼展示了一個簡單的 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 中讀取一個新幀
            ByteBuf buf = in.readBytes(frameLength);
            //將該幀新增到已被解碼的訊息列表中
            out.add(buf);
        }
    }
}

下述程式碼展示了使用 EmbeddedChannel 的對於前面程式碼的測試

public class FixedLengthFrameDecoderTest {
    
    @Test
    public void testFrameDecoded() {
        //建立一個 ByteBuf,並儲存 9 位元組
        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));
        //將資料寫入 EmbeddedChannel
        System.out.println(channel.writeInbound(input.retain()));//true
        //標記 Channel 為已完成狀態
        System.out.println(channel.finish());//true

        //讀取所生成的訊息,並且驗證是否有 3 幀,其中每幀都為 3 位元組
        ByteBuf read = channel.readInbound();
        System.out.println(buf.readSlice(3).equals(read));// true

        read = channel.readInbound();
        System.out.println(buf.readSlice(3).equals(read));// true
        read.release();

        read = channel.readInbound();
        System.out.println(buf.readSlice(3).equals(read));// true
        read.release();

        System.out.println(channel.readInbound() == null);// true
        buf.release();
    }

    @Test
    public void testFramesDescode2() {
        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));
        //返回 false,因為沒有一個完整的可供讀取的幀
        System.out.println(channel.writeInbound(input.readBytes(2)));// false
        System.out.println(channel.writeInbound(input.readBytes(7)));// true

        System.out.println(channel.finish());// true
        ByteBuf read = channel.readInbound();
        System.out.println(buf.readSlice(3) == read);// false
        read.release();

        read = channel.readInbound();
        System.out.println(buf.readSlice(3) == read);// false
        read.release();

        read = channel.readInbound();
        System.out.println(buf.readSlice(3) == read);// false
        read.release();

        System.out.println(channel.readInbound() == null);// true
        buf.release();
    }
}

2. 測試入站訊息

測試出站訊息的處理過程和剛才所看到的類似,在下面的例子中,我們將會展示如何使用 EmbeddedChannel 來測試另一個編碼器形式的 ChannelOutboundHandler,編碼器是一種將一種訊息格式轉換為另一種的元件

該示例將會按照下列方式工作:

  • 持有 AbsIntegerEncoder 的 EmbeddedChannel 將會以 4 位元組的負整數的形式寫出站資料
  • 編碼器將從傳入的 ByteBuf 中讀取每個負整數,並將會呼叫 Math.abs() 方法來獲取其絕對值
  • 編碼器將會把每個負整數的絕對值寫到 ChannelPipeline 中

下述程式碼展示了這個邏輯

public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {
    @Override
    protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        while (msg.readableBytes() >= 4) {
            //從輸入的 ByteBuf 中讀取下一個整數,並且計算其絕對值
            int value = Math.abs(msg.readInt());
            //將該整數寫入到編碼訊息的 List 中
            out.add(value);
        }
    }
}

使用 EmbeddedChannel 來測試程式碼

public class AbsIntegerEncoderTest {
    @Test
    public void testEncoded() {
        ByteBuf buf = Unpooled.buffer();
        for (int i = 1; i < 10; i++) {
            buf.writeInt(i * -1);
        }
        // 建立一個 EmbeddedChanel,並安裝一個要測試的 AbsIntegerEncoder
        EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());
        // 寫入 ByteBuf,呼叫 readOutbound() 方法將會產生資料
        System.out.println(channel.writeOutbound(buf));
        System.out.println(channel.finish());

        channel.readOutbound();
        for (int i = 1; i < 10; i++) {
            int temp = channel.readOutbound();
            System.out.println(temp);
        }
        System.out.println(channel.readOutbound() == null);
    }
}

下面是程式碼中執行的步驟。

  • 將 4 位元組的負整數寫到一個新的 ByteBuf 中
  • 建立一個 EmbeddedChannel,併為它分配一個 AbsIntegerEncoder
  • 呼叫 EmbeddedChannel 上的 writeOutbound()方法來寫入該 ByteBuf
  • 標記該 Channel 為已完成狀態
  • 從 EmbeddedChannel 的出站端讀取所有的整數,並驗證是否只產生了絕對值

測試異常處理

應用程式通常需要執行比轉換資料更加複雜的任務。例如,你可能需要處理格式不正確的輸 入或者過量的資料。在下一個示例中,如果所讀取的位元組數超出了某個特定的限制,我們將會丟擲一個 TooLongFrameException,這是一種經常用來防範資源被耗盡的方法

實現的程式碼如下

// 擴充套件 ByteToMessageDecoder 以將入站位元組碼為訊息
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) {
            // 如果該幀太大,則丟棄它並丟擲一個 TooLongFrameException
            in.clear();
            throw new TooLongFrameException();
        }
        // 否則,從 ByteBuf 中讀取一個新的幀
        ByteBuf buf = in.readBytes(readableBytes);
        // 該幀新增到解碼訊息的List中
        out.add(buf);
    }
}

再使用 EmbeddedChannel 來測試這段程式碼

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,並向其安裝一個幀大小為 3 位元組的 FixedLengthFrameDecoder
        EmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3));

        System.out.println(channel.writeInbound(input.readBytes(2)));
        try {
            // 寫入一個 4 位元組大小的幀,並捕獲預期的異常
            channel.writeInbound(input.readBytes(4));
        } catch (TooLongFrameException e) {
            e.printStackTrace();
        }

        // 寫入剩餘的 2 位元組,會產生一個有效幀
        System.out.println(channel.writeInbound(input.readBytes(3)));//true
        System.out.println(channel.finish());

        // 讀取產生的訊息,並且驗證值
        ByteBuf read = channel.readInbound();
        System.out.println(read.equals(buf.readSlice(2)));//true
        read.release();

        read = channel.readInbound();
        System.out.println(read.equals(buf.skipBytes(4).readSlice(3)));//true
        read.release();
        buf.release();

    }
}

相關文章