讀古今文學網 > Netty實戰 > 第9章 單元測試 >

第9章 單元測試

本章主要內容

  • 單元測試
  • EmbeddedChannel概述
  • 使用EmbeddedChannel測試ChannelHandler

ChannelHandler是Netty應用程序的關鍵元素,所以徹底地測試它們應該是你的開發過程的一個標準部分。最佳實踐要求你的測試不僅要能夠證明你的實現是正確的,而且還要能夠很容易地隔離那些因修改代碼而突然出現的問題。這種類型的測試叫作單元測試。

雖然單元測試沒有統一的定義,但是大多數的從業者都有基本的共識。其基本思想是,以盡可能小的區塊測試你的代碼,並且盡可能地和其他的代碼模塊以及運行時的依賴(如數據庫和網絡)相隔離。如果你的應用程序能通過測試驗證每個單元本身都能夠正常地工作,那麼在出了問題時將可以更加容易地找出根本原因。

在本章中,我們將學習一種特殊的Channel實現——EmbeddedChannel,它是Netty專門為改進針對ChannelHandler的單元測試而提供的。

因為正在被測試的代碼模塊或者單元將在它正常的運行時環境之外被執行,所以你需要一個框架或者腳手架以便在其中運行它。在我們的例子中,我們將使用JUnit 4作為我們的測試框架,所以你需要對它的用法有一個基本的瞭解。如果它對你來說比較陌生,不要害怕;雖然它功能強大,但卻很簡單,你可以在JUnit的官方網站(www.junit.org)上找到你所需要的所有信息。

你可能會發現回顧前面關於ChannelHandler的章節很有用,因為這將為我們的示例提供素材。

9.1 EmbeddedChannel概述

你已經知道,可以將ChannelPipeline中的ChannelHandler實現鏈接在一起,以構建你的應用程序的業務邏輯。我們已經在前面解釋過,這種設計支持將任何潛在的複雜處理過程分解為小的可重用的組件,每個組件都將處理一個明確定義的任務或者步驟。在本章中,我們還將展示它是如何簡化測試的。

Netty提供了它所謂的Embedded傳輸,用於測試ChannelHandler。這個傳輸是一種特殊的Channel實現——EmbeddedChannel——的功能,這個實現提供了通過ChannelPipeline傳播事件的簡便方法。

這個想法是直截了當的:將入站數據或者出站數據寫入到EmbeddedChannel中,然後檢查是否有任何東西到達了ChannelPipeline的尾端。以這種方式,你便可以確定消息是否已經被編碼或者被解碼過了,以及是否觸發了任何的ChannelHandler動作。

表9-1中列出了EmbeddedChannel的相關方法。

表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方法

入站數據由ChannelInboundHandler處理,代表從遠程節點讀取的數據。出站數據由ChannelOutboundHandler處理,代表將要寫到遠程節點的數據。根據你要測試的Channel-Handler,你將使用*Inbound或者*Outbound方法對,或者兼而有之。

圖9-1展示了使用EmbeddedChannel的方法,數據是如何流經ChannelPipeline的。你可以使用writeOutbound方法將消息寫到Channel中,並通過ChannelPipeline沿著出站的方向傳遞。隨後,你可以使用readOutbound方法來讀取已被處理過的消息,以確定結果是否和預期一樣。 類似地,對於入站數據,你需要使用writeInboundreadInbound方法。

在每種情況下,消息都將會傳遞過ChannelPipeline,並且被相關的ChannelInbound-Handler或者ChannelOutboundHandler處理。如果消息沒有被消費,那麼你可以使用readInbound或者readOutbound方法來在處理過了這些消息之後,酌情把它們從Channel中讀出來。

圖9-1 EmbeddedChannel的數據流

接下來讓我們仔細看看這兩種場景,以及它們是如何應用於測試你的應用程序邏輯的吧。

9.2 使用EmbeddedChannel測試ChannelHandler

在這一節中,我們將講解如何使用EmbeddedChannel來測試ChannelHandler

JUnit斷言

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

import static org.junit.Assert.*;

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

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

9.2.1 測試入站消息

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

圖9-2 通過FixedLengthFrameDecoder解碼

正如可以從圖9-2右側的幀看到的那樣,這個特定的解碼器將產生固定為3字節大小的幀。因此,它可能會需要多個事件來提供足夠的字節數以產生一個幀。

最終,每個幀都會被傳遞給ChannelPipeline中的下一個ChannelHandler

該解碼器的實現,如代碼清單9-1所示。

代碼清單9-1 FixedLengthFrameDecoder

public class FixedLengthFrameDecoder extends ByteToMessageDecoder {   ← --  擴展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);  ← --  從ByteBuf 中讀取一個新幀
      out.add(buf);  ← --  將該幀添加到已被解碼的消息列表中 
    }
  }
}  

現在,讓我們創建一個單元測試,以確保這段代碼將按照預期執行。正如我們前面所指出的,即使是在簡單的代碼中,單元測試也能幫助我們防止在將來代碼重構時可能會導致的問題,並且能在問題發生時幫助我們診斷它們。

代碼清單9-2展示了一個使用EmbeddedChannel的對於前面代碼的測試。

代碼清單9-2 測試FixedLengthFrameDecoder

public class FixedLengthFrameDecoderTest {  ← --  使用了註解@Test 標注,因此JUnit 將會執行該方法
  @Test 
  public void testFramesDecoded {  ← --  第一個測試方法:testFramesDecoded
    ByteBuf buf = Unpooled.buffer;   ← -- 創建一個ByteBuf,並存儲9 字節
    for (int i = 0; i < 9; i++) {
       buf.writeByte(i);
    }
    ByteBuf input = buf.duplicate;
    EmbeddedChannel channel = new EmbeddedChannel(    ← -- 創建一個EmbeddedChannel,並添加一個FixedLengthFrameDecoder,其將以3 字節的幀長度被測試
      new FixedLengthFrameDecoder(3));
    // write bytes
    assertTrue(channel.writeInbound(input.retain));    ← -- 將數據寫入Embedded-Channel
    assertTrue(channel.finish);  ← -- 標記Channel為已完成狀態 

    // read messages  ← -- 讀取所生成的消息,並且驗證是否有3 幀(切片),其中每幀(切片)都為3 字節
    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 {  ← --  第二個測試方法: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)));   ← --  返回false,因為沒有一個完整的可供讀取的幀
    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個ByteBuf,每個都包含了3字節。需要注意的是,僅通過一次對writeInbound方法的調用,ByteBuf是如何被填充了9個可讀字節的。在此之後,通過執行finish方法,將EmbeddedChannel標記為了已完成狀態。最後,通過調用readInbound方法,從Embedded-Channel中正好讀取了3個幀和一個null

testFramesDecoded2方法也是類似的,只有一處不同:入站ByteBuf是通過兩個步驟寫入的。當writeInbound(input.readBytes(2))被調用時,返回了false。為什麼呢?正如同表9-1中所描述的,如果對readInbound的後續調用將會返回數據,那麼write-Inbound方法將會返回true。但是只有當有3個或者更多的字節可供讀取時,FixedLength-FrameDecoder才會產生輸出。該測試剩下的部分和testFramesDecoded是相同的。

9.2.2 測試出站消息

測試出站消息的處理過程和剛才所看到的類似。在下面的例子中,我們將會展示如何使用EmbeddedChannel來測試一個編碼器形式的ChannelOutboundHandler,編碼器是一種將一種消息格式轉換為另一種的組件。你將在下一章中非常詳細地學習編碼器和解碼器,所以現在我們只需要簡單地提及我們正在測試的處理器——AbsIntegerEncoder,它是Netty的MessageToMessageEncoder的一個特殊化的實現,用於將負值整數轉換為絕對值。

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

  • 持有AbsIntegerEncoderEmbeddedChannel將會以4字節的負整數的形式寫出站數據;
  • 編碼器將從傳入的ByteBuf中讀取每個負整數,並將會調用Math.abs方法來獲取其絕對值;
  • 編碼器將會把每個負整數的絕對值寫到ChannelPipeline中。

圖9-3展示了該邏輯。

圖9-3 通過AbsIntegerEncoder編碼

代碼清單9-3實現了這個邏輯,如圖9-3所示。encode方法將把產生的值寫到一個List中。

代碼清單9-3 AbsIntegerEncoder

public class AbsIntegerEncoder extends
  MessageToMessageEncoder<ByteBuf> {   ← --  擴展MessageToMessageEncoder 以將一個消息編碼為另外一種格式
  @Override
  protected void encode(ChannelHandlerContext channelHandlerContext,
    ByteBuf in, List<Object> out) throws Exception {
![..\tu\p45-2.tif{25}](/api/storage/getbykey/original?key=17058221f3a645320473)    while (in.readableBytes >= 4) {   ← -- 檢查是否有足夠的字節用來編碼
      int value = Math.abs(in.readInt);  ← --  從輸入的ByteBuf中讀取下一個整數,並且計算其絕對值
      out.add(value);  ← --  將該整數寫入到編碼消息的List 中
    }
  }
}  

代碼清單9-4使用了EmbeddedChannel來測試代碼。

代碼清單9-4 測試AbsIntegerEncoder

public class AbsIntegerEncoderTest {
  @Test
  public void testEncoded {
    ByteBuf buf = Unpooled.buffer;   ← --  ❶創建一個ByteBuf,並且寫入9 個負整數
    for (int i = 1; i < 10; i++) {
      buf.writeInt(i * -1);
    }

    EmbeddedChannel channel = new EmbeddedChannel( ← -- ❷創建一個EmbeddedChannel,並安裝一個要測試的AbsIntegerEncoder
      new AbsIntegerEncoder);
    assertTrue(channel.writeOutbound(buf));  ← -- ❸寫入ByteBuf,並斷言調用readOutbound方法將會產生數據 
    assertTrue(channel.finish);  ← -- ❹將該Channel
標記為已完成狀態

    // read bytes 
    for (int i = 1; i < 10; i++) {  ← -- ❺讀取所產生的消息,並斷言它們包含了對應的絕對值
      assertEquals(i, channel.readOutbound);
    }
    assertNull(channel.readOutbound);
  }
}  

下面是代碼中執行的步驟。

❶ 將4字節的負整數寫到一個新的ByteBuf中。

❷ 創建一個EmbeddedChannel,並為它分配一個AbsIntegerEncoder

❸ 調用EmbeddedChannel上的writeOutbound方法來寫入該ByteBuf

❹ 標記該Channel為已完成狀態。

❺ 從EmbeddedChannel的出站端讀取所有的整數,並驗證是否只產生了絕對值。

9.3 測試異常處理

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

在圖9-4中,最大的幀大小已經被設置為3字節。如果一個幀的大小超出了該限制,那麼程序將會丟棄它的字節,並拋出一個TooLongFrameException。位於ChannelPipeline中的其他ChannelHandler可以選擇在exceptionCaught方法中處理該異常或者忽略它。

圖9-4 通過FrameChunkDecoder解碼

其實現如代碼清單9-5所示。

代碼清單9-5 FrameChunkDecoder

public class FrameChunkDecoder extends ByteToMessageDecoder {   ← --  擴展ByteToMessage-Decoder 以將入站字節解碼為消息
  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  ← --  如果該幀太大,則丟棄它並拋 出一個TooLongFrameException……
      in.clear;
      throw new TooLongFrameException;
    }
    ByteBuf buf = in.readBytes(readableBytes);  ← -- ……否則,從ByteBuf 中讀取一個新的幀  
    out.add(buf);   ← --  將該幀添加到解碼消息的List 中
  }
}  

我們再使用EmbeddedChannel來測試一次這段代碼,如代碼清單9-6所示。

代碼清單9-6 測試FrameChunkDecoder

public class FrameChunkDecoderTest {
  @Test
  public void testFramesDecoded {
    ByteBuf buf = Unpooled.buffer;   ← --  創建一個ByteBuf,並向它寫入9 字節
    for (int i = 0; i < 9; i++) {
      buf.writeByte(i);
    }
      ByteBuf input = buf.duplicate;

    EmbeddedChannel channel = new EmbeddedChannel(
      new FrameChunkDecoder(3));  ← --  創建一個EmbeddedChannel,並向其安裝一個幀大小為3 字節
的FixedLengthFrameDecoder
    assertTrue(channel.writeInbound(input.readBytes(2)));  ← -- 向它寫入2 字節,並斷言它們將會產生一個新幀 
    try {
      channel.writeInbound(input.readBytes(4));  ← -- 寫入一個4 字節大小的幀,並捕獲預期的TooLongFrameException
      Assert.fail;  ← -- 如果上面沒有拋出異常,那麼就會到達這個斷言,並且測試失敗
    } catch (TooLongFrameException e) {
      // expected exception
    }
    assertTrue(channel.writeInbound(input.readBytes(3)));  ← -- 寫入剩餘的2 字節,並斷言將會產生一個有效幀 
    assertTrue(channel.finish);   ← -- 將該Channel 標記為已完成狀態

    // 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中並拋出[1]。這使得可以容易地測試出一個Exception是否在處理數據的過程中已經被處理了。

這裡介紹的測試方法可以用於任何能拋出ExceptionChannelHandler實現。

9.4 小結

使用JUnit這樣的測試工具來進行單元測試是一種非常行之有效的方式,它能保證你的代碼的正確性並提高它的可維護性。在本章中,你學習了如何使用Netty提供的測試工具來測試你自定義的ChannelHandler

在接下來的章節中,我們將專注於使用Netty編寫真實世界的應用程序。我們不會再提供任何進一步的測試代碼示例了,所以我們希望你將這裡所展示的測試方法的重要性牢記於心。


[1] 需要注意的是,如果該類實現了exceptionCaught方法並處理了該異常,那麼它將不會被catch塊所捕獲。