Chapter 9. Unit testing

This chapter covers

  • Unit testing
  • Overview of EmbeddedChannel
  • Testing ChannelHandlers with EmbeddedChannel

ChannelHandlers are the critical elements of a Netty application, so testing them thoroughly should be a standard part of your development process. Best practices dictate that you test not only to prove that your implementation is correct, but also to make it easy to isolate problems that crop up as code is modified. This type of testing is called unit testing.

Although there’s no universal definition of unit testing, most practitioners agree on the fundamentals. The basic idea is to test your code in the smallest possible chunks, isolated as much as possible from other code modules and from runtime dependencies such as databases and networks. If you can verify through testing that each unit works correctly by itself, it will be much easier to find the culprit when something goes awry.

In this chapter we’ll study a special Channel implementation, EmbeddedChannel, that Netty provides specifically to facilitate unit testing of ChannelHandlers.

Because the code module or unit being tested is going to be executed outside its normal runtime environment, you need a framework or harness within which to run it. In our examples we’ll use JUnit 4 as our testing framework, so you’ll need a basic understanding of its use. If it’s new to you, have no fear; though powerful it’s simple, and you’ll find all the information you need on the JUnit website (www.junit.org).

You may find it useful to review the previous chapters on ChannelHandler and codecs, as these will provide the material for our examples.

9.1. Overview of EmbeddedChannel

You already know that ChannelHandler implementations can be chained together in a ChannelPipeline to build up your application’s business logic. We explained previously that this design supports the decomposition of potentially complex processing into small and reusable components, each of which handles a well-defined task or step. In this chapter we’ll show you how it simplifies testing as well.

Netty provides what it calls an embedded transport for testing ChannelHandlers. This transport is a feature of a special Channel implementation, EmbeddedChannel, which provides a simple way to pass events through the pipeline.

The idea is straightforward: you write inbound or outbound data into an EmbeddedChannel and then check whether anything reached the end of the ChannelPipeline. In this way you can determine whether messages were encoded or decoded and whether any ChannelHandler actions were triggered.

The relevant methods of EmbeddedChannel are listed in table 9.1.

Table 9.1. Special EmbeddedChannel methods

Name

Responsibility

writeInbound(
Object... msgs)
Writes an inbound message to the EmbeddedChannel. Returns true if data can be read from the EmbeddedChannel via readInbound().
readInbound() Reads an inbound message from the EmbeddedChannel. Anything returned traversed the entire ChannelPipeline. Returns null if nothing is ready to read.
writeOutbound(
Object... msgs)
Writes an outbound message to the EmbeddedChannel. Returns true if something can now be read from the EmbeddedChannel via readOutbound().
readOutbound() Reads an outbound message from the EmbeddedChannel. Anything returned traversed the entire ChannelPipeline. Returns null if nothing is ready to read.
finish() Marks the EmbeddedChannel as complete and returns true if either inbound or outbound data can be read. This will also call close() on the EmbeddedChannel.

Inbound data is processed by ChannelInboundHandlers and represents data read from the remote peer. Outbound data is processed by ChannelOutboundHandlers and represents data to be written to the remote peer. Depending on the ChannelHandler you’re testing, you’ll use the *Inbound() or *Outbound() pairs of methods, or perhaps both.

Figure 9.1 shows how data flows through the ChannelPipeline using the methods of EmbeddedChannel. You can use writeOutbound() to write a message to the Channel and pass it through the ChannelPipeline in the outbound direction. Subsequently you can read the processed message with readOutbound() to determine whether the result is as expected. Similarly, for inbound data you use writeInbound() and readInbound().

Figure 9.1. EmbeddedChannel data flow

In each case, messages are passed through the ChannelPipeline and processed by the relevant ChannelInboundHandlers or ChannelOutboundHandlers. If the message isn’t consumed, you can use readInbound() or readOutbound() as appropriate to read the messages out of the Channel after processing them.

Let’s take a closer look at both scenarios and see how they apply to testing your application logic.

9.2. Testing ChannelHandlers with EmbeddedChannel

In this section we’ll explain how to test a ChannelHandler with EmbeddedChannel.

JUnit assertions

The class org.junit.Assert provides many static methods for use in tests. A failed assertion will cause an exception to be thrown and will terminate the currently executing test. The most efficient way to import these assertions is by way of an import static statement:

import static org.junit.Assert.*;

Once you have done this you can call the Assert methods directly:

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

9.2.1. Testing inbound messages

Figure 9.2 represents a simple ByteToMessageDecoder implementation. Given sufficient data, this will produce frames of a fixed size. If not enough data is ready to read, it will wait for the next chunk of data and check again whether a frame can be produced.

Figure 9.2. Decoding via FixedLengthFrameDecoder

As you can see from the frames on the right side of the figure, this particular decoder produces frames with a fixed size of 3 bytes. Thus it may require more than one event to provide enough bytes to produce a frame.

Finally, each frame will be passed to the next ChannelHandler in the ChannelPipeline.

The implementation of the decoder is shown in the following listing.

Listing 9.1. FixedLengthFrameDecoder

Now let’s create a unit test to make sure this code works as expected. As we pointed out earlier, even in simple code, unit tests help to prevent problems that might occur if the code is refactored in the future and to diagnose them if they do.

This listing shows a test of the preceding code using EmbeddedChannel.

Listing 9.2. Testing the FixedLengthFrameDecoder

The method testFramesDecoded() verifies that a ByteBuf containing 9 readable bytes is decoded into 3 ByteBufs, each containing 3 bytes. Notice how the ByteBuf is populated with 9 readable bytes in one call of writeInbound(). After this, finish() is executed to mark the EmbeddedChannel complete. Finally, readInbound() is called to read exactly three frames and a null from the EmbeddedChannel.

The method testFramesDecoded2() is similar, with one difference: the inbound ByteBufs are written in two steps. When writeInbound(input.readBytes(2)) is called, false is returned. Why? As stated in table 9.1, writeInbound() returns true if a subsequent call to readInbound() would return data. But the FixedLengthFrameDecoder will produce output only when three or more bytes are readable. The rest of the test is identical to testFramesDecoded().

9.2.2. Testing outbound messages

Testing the processing of outbound messages is similar to what you’ve just seen. In the next example we’ll show how you can use EmbeddedChannel to test a ChannelOutboundHandler in the form of an encoder, a component that transforms one message format to another. You’ll study encoders and decoders in great detail in the next chapter, so for now we’ll just mention that the handler we’re testing, AbsIntegerEncoder, is a specialization of Netty’s MessageToMessageEncoder that converts negative-valued integers to absolute values.

The example will work as follows:

  • An EmbeddedChannel that holds an AbsIntegerEncoder will write outbound data in the form of 4-byte negative integers.
  • The decoder will read each negative integer from the incoming ByteBuf and will call Math.abs() to get the absolute value.
  • The decoder will write the absolute value of each integer to the ChannelHandlerPipeline.

Figure 9.3 shows the logic.

Figure 9.3. Encoding via AbsIntegerEncoder

The next listing implements this logic, illustrated in figure 9.3. The encode() method writes the produced values to a List.

Listing 9.3. AbsIntegerEncoder

The next listing tests the code using EmbeddedChannel.

Listing 9.4. Testing the AbsIntegerEncoder

Here are the steps executed in the code:

  1. Writes negative 4-byte integers to a new ByteBuf.
  2. Creates an EmbeddedChannel and assigns an AbsIntegerEncoder to it.
  3. Calls writeOutbound() on the EmbeddedChannel to write the ByteBuf.
  4. Marks the channel finished.
  5. Reads all the integers from the outbound side of the EmbeddedChannel and verifies that only absolute values were produced.

9.3. Testing exception handling

Applications usually have additional tasks to execute beyond transforming data. For example, you may need to handle malformed input or an excessive volume of data. In the next example we’ll throw a TooLongFrameException if the number of bytes read exceeds a specified limit. This is an approach often used to guard against resource exhaustion.

In figure 9.4 the maximum frame size has been set to 3 bytes. If the size of a frame exceeds that limit, its bytes are discarded and a TooLongFrameException is thrown. The other ChannelHandlers in the pipeline can either handle the exception in exceptionCaught() or ignore it.

Figure 9.4. Decoding via FrameChunkDecoder

The implementation is shown in the following listing.

Listing 9.5. FrameChunkDecoder

Again, we’ll test the code using EmbeddedChannel.

Listing 9.6. Testing FrameChunkDecoder

At first glance this looks quite similar to the test in listing 9.2, but it has an interesting twist; namely, the handling of the TooLongFrameException. The try/catch block used here is a special feature of EmbeddedChannel. If one of the write* methods produces a checked Exception, it will be thrown wrapped in a RuntimeException.[1] This makes it easy to test whether an Exception was handled during processing of the data.

1

Note that if the class implements exceptionCaught() and handles the exception, then it will not be caught by the catch block.

The testing approach illustrated here can be used with any ChannelHandler implementation that throws an Exception.

9.4. Summary

Unit testing with a test harness such as JUnit is an extremely effective way to guarantee the correctness of your code and enhance its maintainability. In this chapter you learned how to use the testing tools provided by Netty to test your custom ChannelHandlers.

In the next chapters we’ll focus on writing real-world applications with Netty. We won’t be presenting any further examples of test code, so we hope you’ll keep in mind the importance of the testing approach we’ve demonstrated here.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset