分享

Netty4实战第十章:Netty应用的单元测试

 柠檬冰啡咖 2017-12-28

本章主要内容:

  • 单元测试
  • EmbeddedChannel
  前面的章节我们学习了如何实现自己的ChannelHandler来实际处理接收的数据和发送的数据。但是如何保证这些ChannelHandler是正确的,总不能上线再去验证吧?
  还好Netty提供了两个类帮助开发者测试ChannelHandler。学习完这一章,就能学会如何测试你的项目,提高应用的健壮性。
  本章使用的单元测试框架是JUnit 4,这可是个神器,搞Java的应该都知道的吧。它虽然简单,但是功能很强大。经过前面的学习,大家应该可以实现自己的ChannelHandler了,这一章我们主要学习如何测试它。

一、前言

  我们知道Netty的ChannelHandler总共有两大类,一类处理收到的数据,一类处理发送的数据,通过ChannelPipeline可以很容易的将这两大类ChannelHandler结合在一起。ChannelHandler算是使用了常用设计模式中的责任链模式,可以很方便的复用里面的实现。ChannelHandler只处理事件,逻辑很简单,也很清晰,并且方便进行测试。

  测试ChannelHandler一般建议使用嵌入式传输方式,前面说过,这种传输方式不会真正走网络,但是很容易传输事件,从而测试你的ChannelHandler实现。这种传输方式提供了一个特殊的Channel实现,名字叫EmbeddedChannel。

  但是它是如何工作的呢?很简单,可以向EmbeddedChannel写数据模拟收到数据或发送数据,然后检查是否经过ChannelPipeline。这样就可以检查数据是否被编码解码或者触发了什么事件。

  下表列出了AbstractEmbeddedChannel提供的常用方法。

名称

描述

writeInbound(…)

向Channel写入数据,模拟Channel收到数据,也就
是说这些数据会经过ChannelPipeline的ChannelInboundHandler

readInbound(…)

从EmbeddedChannel中读数据,返回经过ChannelPipeline中的
ChannelInboundHandler的数据,如果没有数据则返回null
ChannelInboundHandler的数据,如果没有数据则返回null

writeOutbound(…)

向Channel写入数据,模拟Channel发送数据,也就
是说这些数据会经过ChannelPipeline的ChannelOutboundHandler

readOutbound(…)

从EmbeddedChannel中读数据,返回经过ChannelPipeline中的
ChannelOutboundHandler的数据,如果没有数据则返回null
ChannelOutboundHandler的数据,如果没有数据则返回null

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,主要逻辑就是每次收到数据后将数据分成数量固定的组,如果数据不够就不分然后等到下次收到数据的时候再检查是否数据足够,如下图所示。


  从上图可以看出来,主要逻辑很简单,就是读取指定数量数据然后分组,完整代码如下。

  1. import java.util.List;  
  2.   
  3. public class FixedLengthFrameDecoder extends ByteToMessageDecoder {  
  4.   
  5.     //每组数据的长度  
  6.     private final int frameLength;  
  7.   
  8.     public FixedLengthFrameDecoder(int frameLength) {  
  9.         if (frameLength <= 0) {  
  10.             throw new IllegalArgumentException("frameLength must be a positive integer: " + frameLength);  
  11.         }  
  12.         this.frameLength = frameLength;  
  13.     }  
  14.   
  15.     @Override  
  16.     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {  
  17.         //检查数据是否足够  
  18.         while (in.readableBytes() >= frameLength) {  
  19.             //读取指定长度的数据  
  20.             ByteBuf buf = in.readBytes(frameLength);  
  21.             //添加到列表中  
  22.             out.add(buf);  
  23.         }  
  24.     }  
  25. }  
  一般来说,我们实现了一段主要的逻辑,最好就使用单元测试验证一下。即使你能保证你现在写的代码没什么问题,但你后面可能会重构你的代码,所以写一段单元测试,重构之后只要再跑一下单元测试,就能检查重构是否出现问题。单元测试可以帮助开发者发现很多问题,预防在生产环境出现重大问题。

  下面我们来测试一下刚才我们实现的解码器。

  1. package com.nan.netty.test;  
  2.   
  3. import io.netty.buffer.ByteBuf;  
  4. import io.netty.buffer.Unpooled;  
  5. import io.netty.channel.embedded.EmbeddedChannel;  
  6. import org.junit.Assert;  
  7. import org.junit.Test;  
  8.   
  9. public class FixedLengthFrameDecoderTest {  
  10.   
  11.     @Test  
  12.     public void testFramesDecoded() {  
  13.         ByteBuf buf = Unpooled.buffer();  
  14.         for (int i = 0; i < 9; i++) {  
  15.             buf.writeByte(i);  
  16.         }  
  17.         ByteBuf input = buf.duplicate();  
  18.         EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));  
  19.   
  20.         //验证写数据返回True  
  21.         Assert.assertTrue(channel.writeInbound(input.readBytes(9)));  
  22.         Assert.assertTrue(channel.finish());  
  23.         //每次读3个数据  
  24.         Assert.assertEquals(buf.readBytes(3), channel.readInbound());  
  25.         Assert.assertEquals(buf.readBytes(3), channel.readInbound());  
  26.         Assert.assertEquals(buf.readBytes(3), channel.readInbound());  
  27.         Assert.assertNull(channel.readInbound());  
  28.     }  
  29.   
  30.     @Test  
  31.     public void testFramesDecoded2() {  
  32.         ByteBuf buf = Unpooled.buffer();  
  33.         for (int i = 0; i < 9; i++) {  
  34.             buf.writeByte(i);  
  35.         }  
  36.         ByteBuf input = buf.duplicate();  
  37.         EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));  
  38.         Assert.assertFalse(channel.writeInbound(input.readBytes(2)));  
  39.         Assert.assertTrue(channel.writeInbound(input.readBytes(7)));  
  40.         Assert.assertTrue(channel.finish());  
  41.         Assert.assertEquals(buf.readBytes(3), channel.readInbound());  
  42.         Assert.assertEquals(buf.readBytes(3), channel.readInbound());  
  43.         Assert.assertEquals(buf.readBytes(3), channel.readInbound());  
  44.         Assert.assertNull(channel.readInbound());  
  45.     }  
  46. }  
  我们看看上边的单元测试的主要逻辑。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的代码如下。
  1. package com.nan.netty.test;  
  2.   
  3. import io.netty.buffer.ByteBuf;  
  4. import io.netty.channel.ChannelHandlerContext;  
  5. import io.netty.handler.codec.MessageToMessageEncoder;  
  6.   
  7. import java.util.List;  
  8.   
  9. public class AbsIntegerEncoder extends MessageToMessageEncoder<ByteBuf> {  
  10.   
  11.     @Override  
  12.     protected void encode(ChannelHandlerContext channelHandlerContext,  
  13.                           ByteBuf in, List<Object> out) throws Exception {  
  14.         while (in.readableBytes() >= 4) {  
  15.             int value = Math.abs(in.readInt());  
  16.             out.add(value);  
  17.         }  
  18.     }  
  19. }  
  可以看出,实现很简单,有整数就读出来,取得绝对值,放入到结果列表中。这么简单的逻辑大家可能会觉得肯定不会出错,不过有没有错误最好还是通过单元测试来证明。
  1. package com.nan.netty.test;  
  2.   
  3. import io.netty.buffer.ByteBuf;  
  4. import io.netty.buffer.Unpooled;  
  5. import io.netty.channel.embedded.EmbeddedChannel;  
  6. import org.junit.Assert;  
  7. import org.junit.Test;  
  8.   
  9. public class AbsIntegerEncoderTest {  
  10.     @Test  
  11.     public void testEncoded() {  
  12.         ByteBuf buf = Unpooled.buffer();  
  13.         for (int i = 1; i < 10; i++) {  
  14.             buf.writeInt(i * -1);  
  15.         }  
  16.         EmbeddedChannel channel = new EmbeddedChannel(new AbsIntegerEncoder());  
  17.         //模拟发送数据  
  18.         Assert.assertTrue(channel.writeOutbound(buf));  
  19.         Assert.assertTrue(channel.finish());  
  20.         //检查是否是正数  
  21.         for (int i = 1; i < 10; i++) {  
  22.             Assert.assertEquals(i, (int) channel.readOutbound());  
  23.         }  
  24.         //没有数据返回null  
  25.         Assert.assertNull(channel.readOutbound());  
  26.     }  
  27. }  

三、测试捕获异常

   有些情况如果收到或发送的数据有问题如数据量不足,这种情况可能需要抛出一个异常。现在我们来测试一个抛出异常的情况,实现一个解码器,主要逻辑就是收到的数据长度超过限制时,就抛出异常TooLongFrameException

  收到数据超过指定长度时就清除这些数据并抛出TooLongFrameException异常,然后下一个ChannelHandler通过exceptionCaught(…)方法就可以捕获这个异常或者忽略。
  1. package com.nan.netty.test;  
  2.   
  3. import io.netty.buffer.ByteBuf;  
  4. import io.netty.channel.ChannelHandlerContext;  
  5. import io.netty.handler.codec.ByteToMessageDecoder;  
  6. import io.netty.handler.codec.TooLongFrameException;  
  7.   
  8. import java.util.List;  
  9.   
  10. public class FrameChunkDecoder extends ByteToMessageDecoder {  
  11.     private final int maxFrameSize;  
  12.   
  13.     public FrameChunkDecoder(int maxFrameSize) {  
  14.         this.maxFrameSize = maxFrameSize;  
  15.     }  
  16.   
  17.     @Override  
  18.     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {  
  19.         int readableBytes = in.readableBytes();  
  20.         if (readableBytes > maxFrameSize) {  
  21.             in.clear();  
  22.             throw new TooLongFrameException();  
  23.         }  
  24.         ByteBuf buf = in.readBytes(readableBytes);  
  25.         out.add(buf);  
  26.     }  
  27. }  
  单元测试还是选择EmbeddedChannel最好,代码如下。
  1. package com.nan.netty.test;  
  2.   
  3. import io.netty.buffer.ByteBuf;  
  4. import io.netty.buffer.Unpooled;  
  5. import io.netty.channel.embedded.EmbeddedChannel;  
  6. import io.netty.handler.codec.TooLongFrameException;  
  7. import org.junit.Assert;  
  8. import org.junit.Test;  
  9.   
  10. public class FrameChunkDecoderTest {  
  11.   
  12.     @Test  
  13.     public void testFramesDecoded() {  
  14.         ByteBuf buf = Unpooled.buffer();  
  15.         for (int i = 0; i < 9; i++) {  
  16.             buf.writeByte(i);  
  17.         }  
  18.         ByteBuf input = buf.duplicate();  
  19.         EmbeddedChannel channel = new EmbeddedChannel(new FrameChunkDecoder(3));  
  20.         Assert.assertTrue(channel.writeInbound(input.readBytes(2)));  
  21.         try {  
  22.             channel.writeInbound(input.readBytes(4));  
  23.             Assert.fail();  
  24.         } catch (TooLongFrameException e) {  
  25.             System.out.println("Catch TooLongFrameException");  
  26.         }  
  27.         Assert.assertTrue(channel.writeInbound(input.readBytes(3)));  
  28.         Assert.assertTrue(channel.finish());  
  29.         Assert.assertEquals(buf.readBytes(2), channel.readInbound());  
  30.         Assert.assertEquals(buf.skipBytes(4).readBytes(3), channel.readInbound());  
  31.     }  
  32. }  
  这个单元测试可能和前面的看着很像,但是有很明显的区别,就是这里使用了try / catch捕获了异常,可以看到,使用EmbeddedChannel也可以测试这种特殊的需求。例子中虽然测试的是ByteToMessageDecoder,但是很明显,任何ChannelHandler抛出异常的情况都可以使用EmbeddedChannel进行测试。

四、总结

  这一章我们主要学习了单元测试我们实现的ChannelHandler,单元测试框架使用的还是经典的JUnit。测试中使用的Channel类型是EmbeddedChannel,虽然它的实现很简单,但功能是很完善的,可以帮助我们测试自己的ChannelHandler。
  下一章我们开始学习在实际项目中使用Netty,当然后面的应用可能没有单元测试,但是请大家记住,在实际工作中你编写的代码应该是有单元测试的,它的重要性不亚于你的业务代码。



 




  


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多