Channels ▼
RSS

JVM Languages

Testing OO Systems, Part 2: Testing Servlet Implementations


The mock output stream in Listing Three is simpler. The write() method just buffers the characters as they're written, and we can call getOutput() to get that string once the Servlet's done with it.

Lisitng Three

package com.ecmhp.mockobjects;

import java.io.*;
import java.util.*;
import javax.servlet.ServletOutputStream;

public class MockServletOutputStream extends ServletOutputStream
{
	private StringBuilder output = new StringBuilder();
	private LinkedList<Character> charactersReceived = new LinkedList<Character>();
	private boolean returnedEOF = false;
	
	@Override public void write(int b) throws IOException
	{	
		if( returnedEOF == true )	// clear out the previous buffer-full of characters
		{	returnedEOF = false;
			output.setLength(0);
			charactersReceived.clear(); 
		}
		output.append( (char)b );
		charactersReceived.addLast( (char)b );
	}
	
	/** Return all of the output that was sent to this stream. This method
	 *  interacts with read() in that the output buffer is by writing
	 *  a character immediately after read() returned -1.
	 */
	public String getOutput()
	{	return output.toString();
	}
	
	
	/** Read one character from the stream. Return -1 if there's nothing to read.
	 *  Note that if new output is written to the stream after read() has returned
	 *  -1, that new output will be returned until it's exhausted (in which case
	 *  -1 is returned, again). close() has no impact on this behavior.
	 */
	public int read()
	{	if( charactersReceived.size() <= 0 )
		{	returnedEOF = true;
			return -1;
		}
		return charactersReceived.removeFirst();
	}
}

My template for a typical servlet test that uses these classes is shown in Listing Four. The doTest() method first manufactures mocks of the HttpServletRequest and HttpServletResponse objects that we'll pass to the servlet. It then creates instances of our mock input and output streams, and finally arranges for the mock request and response objects to return our versions of the input or output streams as required. For example,

    when( request.getInputStream()   ).thenReturn( in );

says that when the servlet calls request.getInputStream(), the mock will return in, an instance of the MockServletInputStream class we just looked at.

We run the test by calling testDoPost(), passing it our mocks, then we verify that things work — first by checking that sendError() was never called on the response object, and then by checking that the string returned from the request was written verbatim into the response. Of course, your servlet will hardly every do exactly that, but this is template code that I'll modify to actually verify that the output is what I expect it to be.

The second method in Listing Four creates 20 threads, each of which calls the doTest() method we just looked at. The threads are queued up at a CountDownLatch, which works like the starting gate at a racetrack. As the loop creates the threads, it calls countDown() for each thread (which will cause the thread to queue up at the gate). When the count counts down to zero, all the waiting threads are released simultaneously, so they all run more or less in parallel. My hope is that this test will simulate a heavy enough load on the server that it will uncover any threading-related bugs.

Listing Four

package com.ecmhp.tests;

import static org.junit.Assert.*;

import java.io.*;
import java.util.concurrent.CountDownLatch;

import javax.jms.JMSException;
import javax.naming.NamingException;
import javax.servlet.*;
import javax.servlet.http.*;

import org.junit.*;
import org.mockito.Mockito;

public class ServletTest extends Mockito
{
	@Test
	public void doTest() throws Exception
	{
		String requestString = "Request Contents";
		
		HttpServletRequest   request  	= mock(HttpServletRequest.class);
		HttpServletResponse  response 	= mock(HttpServletResponse.class);
		
		MockServletInputStream  in  = new MockServletInputStream( requestString );
		MockServletOutputStream out = new MockServletOutputStream();
		
		in.setDelay(0);	// no delay on read to stress the system as much as possible.
		
		// Use mocks for the input and output streams fetched from the (mock) request and response objects
		// You can also mock other request/response methods (to supply an expected URL, for example).
		//
		when( request.getInputStream()   ).thenReturn( in );
		when( response.getOutputStream() ).thenReturn( out );
        
		//======================================================================
		// Run the test
		//======================================================================

		testObject.testDoPost( request, response );			// test using the mock request/response objects.
		
		//======================================================================
		// TODO: This statement, when running on a thread, does
		// not cause Eclipse to indicate a JUnit error. It does
		// report the problem to the console, however.
		//
		verify(response, never()).sendError( anyInt(), anyString() ); // no error responses sent to client.
		
		assertEquals( request, out.getOutput() );
	}


	@Test
	public void testUsingMultipleThreads() throws Exception
	{
        Thread.sleep( 2000L ); // wait a couple seconds to make sure everything's stopped
        
		final int numThreads = 20;
		final CountDownLatch done = new CountDownLatch( numThreads );
		
		for( int i = 0; i < numThreads; ++i )
		{	
            final int threadID = i; // must be final to be accessible from within inner class
			new Thread()
			{	@Override public void run()
				{	
                    System.err.println("================ Thread " + threadID + " started ================ ");
					Exception cause;
					try
					{
						doTest();
                        System.err.println("================ Thread " + threadID + " terminating normally ================ ");
						return;
					}
					catch (IOException e) 		{cause = e;}
					catch (ServletException e)	{cause = e;}
					catch (Exception e)			{cause = e;}
					finally
					{	done.countDown();
					}
					
					System.err.println("Exception in thread:");
					cause.printStackTrace();
					fail();
				}
			}.start();
		}
		
		done.await();
	}
}


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