Test networked code the easy way

Two techniques to improve your network code testing regimen

Testing network code is awkward. Good unit test suites run quickly so that developers can run the tests after every compile. Test suites must also run reliably such that they consistently catch any errors in the code. However, networked code (for example, code that reads from a URL) proves difficult to test reliably and quickly. Moreover, if the test suite itself makes network calls, the tests will run slowly and unreliably since they now depend on a network and other servers.

Consider a program that downloads, formats, and displays XML data from the Web. A naive test suite for this program would require a running Web server from which to fetch the XML data. But many parts of the program—the XML parser, the formatter, the displayer—could be tested on their own without relying on a network. With that example in mind, in this article I demonstrate two techniques for testing network-related code that avoid using the network when the tests run. I provide in-text code samples, but you can follow along by downloading the complete software.

I begin by describing PrintRSS, a simple network-enabled demonstration program, then discuss how to design the PrintRSS program for easy testing with simple Reader and Writer objects rather than network connections. I finish with a library that allows programmers to synthesize special testurl: URLs that stand in for normal http: URLs, thereby bypassing the network. Note: All tests use the JUnit test framework's assert() methods.

PrintRSS: A demonstration program

PrintRSS, a simple program that reads data from a URL and processes it, serves as a good network code testing demonstration. PrintRSS reads data in the RSS (RDF Site Summary) format, a simple XML data format used for syndicating news feeds. For the this article's purposes, the significant RSS structure is:

  <rss><channel>
    <title>Channel Title</title>
    <item><title>Item 1</title></item>
    <item><title>Item 2</title></item> ...
  </channel></rss>

PrintRSS downloads an RSS document from a URL, formats the contents, then prints the titles to System.out in an easy-to-read display:

  Channel Title
    Item 1
    Item 2

PrintRSS performs four major operations:

  • Opens a connection to the URL
  • Reads in the XML
  • Formats the data
  • Writes to System.out

The PrintRSS program encapsulates all four functions in a single method, printURL(URL). Testing this method, however, proves difficult for two reasons. First, the code relies on data loaded from the URL; if the URL is an http: URL, that involves using the network. And the code has its behavior buried in the side effect of writing to System.out. Considering those problems, how can you design PrintRSS for better testing?

Use readers and writers to encapsulate data

Because most of PrintRSS's logic simply parses and formats the XML code, rather than connecting to the network, you can factor the code so you can independently test the data logic. While refactoring the code may seem daunting, such efforts usually result in better code—both because the code is tested and because the design proves more modular.

With that in mind, you can split off printURL()'s code-parsing and -formatting functions into a new formatReader(Reader, Writer) method exclusively dedicated to taking a Reader object with XML data in it, parsing it, and writing the report out to the supplied Writer.

Testing formatReader(Reader, Writer) now proves simple:

testFormatReaderGoodData():
  String goodRSSData = "<rss><channel>" +
                         "<title>Channel Title</title>" +
                         "<item><title>Item 1</title></item>" +
                         "<item><title>Item 2</title></item>" +
                       "</channel></rss>";
  String goodRSSOutput = "Channel Title\n  Item 1\n  Item 2\n";
  Reader r = new StringReader(goodRSSData);
  Writer w = new StringWriter();
  PrintRSS.formatReader(r, w);
  assertEquals(goodRSSOutput, w.toString());

The example above tests the parser's and formatter's logic without URLs or network connections, just readers and writers. The test example illustrates a useful test technique: creating reader streams with test data contained right in the test code rather than reading the data from a file or the network. StringReader and StringWriter (or ByteArrayInputStream and ByteArrayOutputStream) prove invaluable for embedding test data in unit-test suites.

The unit test above exercises the logic to see what happens when everything works right, but it's equally important to test error handling code for cases when something goes wrong. Next, here's an example of testing with bad data, using a clever JUnit idiom to check that the proper exception throws:

    testFormatReaderBadData():
  String badXMLData = "this is not valid xml data";
  StringReader r = new StringReader(badXMLData);
  try {
    PrintRSS.formatReader(r, new StringWriter());
    fail("should have thrown XML error");
  } catch (XMLParseException ex) {
    // No error, we expected an exception
  }

Again, readers and writers encapsulate data. The main difference: This data causes formatReader() to throw an exception; if the exception does not throw, then JUnit's fail() method is called.

Use Java protocol handlers to test network code

While factoring out nonnetworked methods can make programs like PrintRSS easier to test, such efforts prove insufficient for creating a full test suite. We still want to test the network code itself, particularly any network exception handling. And sometimes it proves inconvenient to factor out to a reader interface; the tested code may rely on a library that only understands URLs. In this section I explain how to test the formatURL(URL, Writer) method that takes a URL, reads the RSS, and writes out the data to a writer.

You can test code that contains URLs in several ways. First, you could use standard http: URLs and point to a working test server. That approach, however, requires a stable Web server—one more component to complicate the testing setup. Second, you could use file: URLs that point to local files. That approach shares a problem with http: URLs: While you can access good test data with file: or http: URLs, it proves difficult to simulate network failures that cause I/O (input/output) exceptions.

A better approach to testing URL code creates a new URL namespace, testurl:, completely under the test program's control—an easy approach with Java protocol handlers.

Implement testurl:

Java protocol handlers allow programmers to implement custom URL protocols with their own code. You'll find a full explanation in Brian Maso's "A New Era for Java Protocol Handlers" (Sun Microsystems, August 2000) and a sample implementation in the source code's org.monkey.nelson.testurl package.

Protocol handlers are simple. First, satisfy the Java requirements for writing a protocol handler, of which there are three important pieces:

  • The TestURLConnection class: A URLConnection implementation that handles the actual methods that return input streams, headers, and so on
  • The Handler class: Turns a testurl: URL into a TestURLConnection
  • The java.protocol.handler.pkgs property: A system property that tells Java where to find the new URL namespace implementation

Second, provide a useful TestURLConnection implementation. In this case, the actual TestURLConnection is hidden from library users; instead, work is done via a helper class, TestURLRegistry. This class has a single method, TestURLRegistry.register(String, InputStream), which associates an input stream with a given URL. For example:

  InputStream inputIS = ...;
  TestURLRegistry.register("data", inputIS);

arranges things so that opening the URL testurl:data returns the inputIS input stream. With the registry, you can easily construct URLs whose output the test program controls. (Note that the input stream data is not copied, so in general, you can use each URL only once.)

Test with good data

We first use testurl: to test the formatURL() method with good input data:

testFormatURLGoodStream():
  InputStream dataIS = new ByteArrayInputStream(goodRSSData.getBytes());
  TestURLRegistry.register("goodData", dataIS);
  URL testURL = new URL("testurl:goodData");
  Writer w = new StringWriter();
  PrintRSS.formatURL(testURL, w);
  assertEquals(goodRSSOutput, w.toString());

The code above uses the same test data from the earlier test, but in this case, associates it to the testurl:goodData URL. The formatURL() method opens and reads that URL, and the test code ensures that the data is correctly read, formatted, and written to the Writer.

Simulate failures

Writing programs that use the network is easy; writing code that correctly handles network failures proves harder. The testurl: can simulate network failures—a major advantage to this approach.

Let's first simulate a failure in which a connection to the URL is simply impossible. Typically, that happens when the remote Web server isn't working. Our testurl: library includes a special URL it understands, testlurl:errorOnConnect, which guarantees an IOException thrown as soon as you connect to the URL. You can use that URL to test whether the program properly handles cases in which the URL cannot be reached:

testFormatURLNoConnection():
  URL noConnectURL = new URL("testurl:errorOnConnect");
  Writer w = new StringWriter();
  try {
    PrintRSS.formatURL(noConnectURL, w);
    fail("Should have thrown IO exception on connect.");
  } catch (IOException ioex) {
  }

The test above ensures that formatURL() correctly throws an IOException if the connection fails. But how do you test for a case where the network connection works at first and then fails during the transaction?

Because you have full control over the input stream associated with the test URL, you can employ any input stream implementation. As an example, you can create a simple BrokenInputStream class that wraps an underlying stream, with a pass-through read() method that allows only a certain number of bytes to be read before throwing an IOException:

testFormatURLBrokenConnection():
  InputStream dataIS = new ByteArrayInputStream(goodRSSData.getBytes());
  InputStream testIS = new BrokenInputStream(dataIS, 99);
  TestURLRegistry.register("brokenStream", testIS);
  URL testURL = new URL("testurl:brokenStream");
        
  Writer w = new StringWriter();
  try {
    PrintRSS.formatURL(testURL, w);
    fail("Should have thrown IO exception on read.");
  } catch (IOException ioex) {
  }

In the code above, the testurl:brokenStream URL is associated with an input stream that returns 99 bytes of good data, then throws an exception. In this case, the test only sees if an exception throws. For a more complex program, you could test that something useful was done with the first successfully read 99 bytes.

Expand on a network testing library

In this article, I demonstrated simple techniques for testing networked code. First, factor code so you can test the logic separately from the network. Second, use a few simple utility classes to provide a testing URL namespace and input streams that simulate network failures. With these techniques it becomes much simpler to test programs that use the network.

This approach, however, has some limitations. The testurl: library is very simple; the TestURLConnection does not support writing to a URL (as you would need for an HTTP POST post operation), nor does it support headers (such as those needed to read an HTTP MIME type). A more complete URLConnection implementation with these features could test complex URL interactions such as SOAP (Simple Object Access Protocol) calls to Web services.

Moreover, this approach only simulates a network, but is not a real network connection. Robust Internet clients must cope with numerous bizarre behaviors including slow connections, hung sockets, and so on. To some extent, you can simulate such failures with more complex InputStream implementations, but testing every network code aspect can only be done with real sockets. However, the techniques described in this article prove sufficient to test most situations that network programs encounter.

Nelson Minar, a software engineer at Google, has six years of Java programming experience. He has built several complex network systems in Java, including Hive, an experimental mobile agent system, and Popular Power, the first distributed computing system for commercial use. It's been two years since Nelson got the unit test religion; he sleeps much better knowing his code is well tested.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more