Channels ▼
RSS

JVM Languages

Testing OO Systems, Part 2: Testing Servlet Implementations


In my last column, Testing OO Systems, Part 1, I talked a lot about the notion of data abstraction in the context of OO systems. In particular, objects should be opaque to the outside world. You talk to them by sending messages, which request that the object does something for you. The main idea is that you should be able to replace a class's implementation with another, completely different, implementation without impacting any of the "client" classes — the classes whose objects use your class. You do that by implementing interfaces that do work rather than get information. The principle is summarized in many ways: "Ask for help, not for information," "Ask the object that has the information to do the work for you" (delegation), "Talk only to your friends" (the Law of Demeter), and so forth. If you follow these principles, then your classes will tend not to have getter/setter functions ("accessors" or "mutators"), simply because they aren't needed any more.

The challenge that I covered in Part 1 is how to test classes that hide their internal implementation and state. The basic solution is to ask the object to do something and then watch what it does. Stimulate the object under test (OUT), then look at the clients (the objects that your OUT talks to). The basic approach is to "mock" (write lightweight simulations of) the client classes, either directly or by using a mocking framework like Mockito. Your mocks either monitor the OUT's behavior (so you can ask them how the OUT behaved after the test runs) or just report incorrect behavior as soon as they detect it.

Last month's examples were pretty simple, though. You could test everything you needed to test by looking at purely external behavior. You could look at the OUT as an impenetrable sphere.

The Donut

More often than not, however, the OUT is more of a donut than a sphere. Outside the ring are the client classes that comprise the users of your OUT. Inside the ring are the objects that your OUT uses internally. For example, it's easy enough to test a Servlet by giving it an artificial URL request and then looking at the string that it outputs. (We'll see how to do that momentarily). However, in order to pull this test off, you'll probably have to control things like the database communication. For example, you might want the Servlet you're testing to use a test version of your database, or even better, you might want to pull the database out of the test entirely and just simulate the existence of the real database with a few lightweight classes.

That last approach is really the best. You don't have to actually get a database up and running in order to test your class, and you even can write and test the class before the database is implemented. If you don't need a working database, your development process won't be "blocked" if the database happens to be busy with other projects. You also won't need to get a real database up and running on your workstation. (Of course, you may want to run tests both with and without the real database, which isn't a problem at all.)

The tricky part of the inside-the-donut parts of the test is that you don't want your object to know that it's being tested. It's just not a valid test if your class has if statements that select between real and test databases or objects because you're not testing the real paths through the code in that case. You should never have to modify an object to test that object.

Testing a Servlet

The easiest way to understand the process is to look at some sample code (which I've stripped down considerably so that the relevant parts won't be lost in the clutter). Let's start by looking at a simple Servlet (Listing One). This servlet handles a simple HTTP post. It reads the body of the post from the request object, then generates a response.

Listing One

package com.holub.experiments;

import javax.servlet.*;
import javax.servlet.http.*;

@WebServlet("/sample")
public class SampleServlet extends HttpServlet
{
	private static final long		serialVersionUID = 1L;
	private static final Logger 	log = Logger.getLogger(TransactionHandler.class);
	
    @Override public void init()
	{	
    	//...
    }
    
    @Override public void destroy()
    {  
    	//...
    }
    
	protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
	{
		InputStream inputStream = request.getInputStream();

		//...
		if( error )
			response.sendError( HttpServletResponse.SC_BAD_REQUEST, "Explain what's wrong with the request" );

		OutputStream outputStream = response.getOutputStream();
		PrintWriter  outputWriter = new PrintWriter( outputStream );
			
		response.setCharacterEncoding("UTF-8");
		response.setContentType		 ("text/xml");
		
		outputWriter.write("The Output");
		
		outputWriter.close();
		inputStream.close();
	}
    
	/** For testing only. Provides public access to doPost */
	public void testDoPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
	{	 doPost(request, response);
	}
}

We can test this servlet by mocking the input stream created by the request to feed the servlet a test string, then run the servlet manually, and finally look at the string that it writes to the output stream to make sure that it's what we expect.

The only oddity is that the actualy doPost(...) method has to be protected, thus I provided a simple public pass-through wrapper at the bottom of the file so that I can call doPost(...) from my test.

The mock input and output streams are defined in Listing Two and Listing Three, respectively..

The input stream's constructor is passed a String, the characters of which are returned, one at a time, from read. The one subtly here is that I've inserted a short, but random, delay into the first read request to simulate network latency.

Listing Two

package com.ecmhp.mockobjects;

import java.io.*;
import javax.servlet.ServletInputStream;
import org.apache.log4j.Logger;
import static org.junit.Assert.*;

/** Simulate a ServletInputStream for testing. A random delay
 *  is put in front of the first call to read() to simulate
 *  network latency.
 * 
 * @author allenh
 */

public class MockServletInputStream extends ServletInputStream
{
	private static Logger log = Logger.getLogger( MockServletInputStream.class );
	
	private MockServletOutputStream out = null;
	private Reader 					inputReader = null;
	private StringBuilder			charactersRead = new StringBuilder();
	
	private boolean needToInsertDelay = true;
	private long	maxDelay = 250;	// defaults to .5 seconds
	
	@Override public int read() throws IOException
	{	
		if( needToInsertDelay )
		{	// Simulate HTTP delays by sleeping (on the first call only).
			
			needToInsertDelay = false;
			if( maxDelay != 0)
			{	try
				{	
					long sleepTime = Math.round(Math.random() * maxDelay );
					log.debug("Inserting " + sleepTime + "ms input delay.");
					Thread.sleep( sleepTime );
				}
				catch (InterruptedException e)
				{	fail("Interrupted from sleep.");
				}
			}
		}
		
		int read = (out != null) ? out.read() : inputReader.read() ;
		
		if( read < 0 )
			needToInsertDelay = true;	// on the next call
		else
			charactersRead.append( (char) read );

		return read;
	}
	
	/** If you use this constructor, the object will get it's input from the
	 *  string.
	 * @param inputString
	 */
	public MockServletInputStream(String inputString)
	{	inputReader = new StringReader(inputString);
	}
	
	/** This constructor causes the input stream to read from the indicated
	 *  reader.
	 * @param inputReader
	 */
	public MockServletInputStream(Reader inputReader)
	{	this.inputReader = inputReader;
	}
	
	/** If you use this constructor, the object will get its input from the
	 *  specified mock output stream. Output is fetched on an ongoing
	 *  basis, that is the current input stream's read() just calls the
	 *  output stream's read(). This method is useful when you're testing
	 *  a POST from the client side, and the output is read before the
	 *  input is generated.
	 * @param out
	 */
	public MockServletInputStream(MockServletOutputStream out)
	{	this.out = out;
	}

	/** Return the characters that have been read by this input stream (so far).
	 */
	public String getInput()
	{
		return charactersRead.toString();
	}
	
	/** Control the amount of delay inserted at the beginning of a read request.
	 *  The delay is a random interval varying from no delay at all up to this
	 *  many milliseconds. The argument can be 0 if you want no delay at all.
	 *  the default (if you don't call this method) is 250 milliseconds.
	 */
	public void setDelay( long maxDelay )
	{	this.maxDelay = maxDelay;
	}
}

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video