Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Test First

This page is under construction. If you find this discussion interesting, please contribute your additions to this page, or add your comments in the discussion area. Consider clicking the "Watch" button to the left so that you will be automatically informed of changes to this page.

Test first is a development process by which tests are written before the code that's being tested. For some, this may seem a little backwards. Quite the contrary. There are a lot of benefits to writing your code this. To start with, you are almost certain to have decent unit test coverage in the end. Further, by defining your tests first, you are defining the contracts that your code needs to implement and encouraging top-down development. Defining the contracts is valuable because it helps you, as the developer, to better understand the problem before you start writing code. Top-down development encourages you to write the code you actually need, rather than the code you think you need.

Consider picking up a copy of Kent Beck's "Test-Driven Development" to gain further insight into the test first process. Incidently, it is not my intention to add to vernacular; "test first" is not intended as a name for the process, but rather is a description.

Wayne's talk on the subject from Eclipse Forum Europe is here in both PowerPoint and PDF formats. Unfortunately, this talk is heavily dependent on demonstrations.

A screencam based on the first demonstration from this talk is available here. Unfortunately, screencam for the other elements of the talk have not yet been recorded.

Unit Testing

For the purposes of this discussion, the style of testing we're referring to is unit testing. This is a term that is becoming more and more overloaded. Simply put, unit testing is about testing your code in the smallest units/fragments/chunks possible. Somewhere along the lines, the notion of a "unit test environment" was coined, refering to a local instance of an application server. For this discussion, this is far too large environment. Generally unit tests are individually small, but can be run in arbitrarily large groups (ideally, unit tests are structured hierarchically and can be invoked at each level of the hierarchy). Ideally unit tests run independent of other processes; specifically, in the ideal world, you don't have to start an application server or database separately to make unit tests work. The word "ideal" is used quite a lot here since reality may present challenges that make these ideals difficult to implement.

Testing in a Framework

Substitution or the use of a test harness are mechanisms that can be used to make unit testing easier. In tests, for example, a Derby database might substitute for a much larger production database.

It's important at each stage of testing to keep in mind what it is that you're testing. In general, you are testing your code, frameworks, and infrastructure. In general, you're not testing other people's stuff. They should have tests for that. In some cases, you may indeed be writing tests for other people's code. In these cases, those tests should test the functionality of that stuff in insolation.

Isolation is the key. Ideally, code is tested in isolation of other code. One way of accomplishing this is through the use of mock objects, or objects that stand in for the "real" objects. It's relatively easy, for example, to build simple mock objects for a servlet-based application. Over time, these mock objects may become more and more complex as the code being tested becomes complex. However, mock objects can very often be evolved along with the code they exist to test.

Consider the following unit test for a servlet (JUnit 3.8.1):

package org.eclipse.servlets.tests;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import junit.framework.TestCase;

import org.eclipse.samples.servlet.AuctionService;
import org.eclipse.samples.servlet.AuctionServlet;
import org.eclipse.servlets.tests.util.MockAuctionService;
import org.eclipse.servlets.tests.util.MockHttpServletRequest;
import org.eclipse.servlets.tests.util.MockHttpServletResponse;
import org.eclipse.servlets.tests.util.MockRequestDispatcher;
import org.eclipse.servlets.tests.util.MockServletContext;

/**
 * This class represents a very early start on some unit 
 * tests against a servlet. This testing uses mock objects
 * and does not require a running application server. As
 * additional tests are added, it is very likely that new
 * functionality will have to be added to the mock objects
 * to stand in for application server behaviour. At all
 * points, remember what it is that you're trying to test.
 * 
 * @author Wayne Beaton
 *
 */
public class AuctionServletTests extends TestCase {
	
	private AuctionServlet servlet;

	@SuppressWarnings("serial")
	@Override
	protected void setUp() throws Exception {
		/*
		 * Create an instance of the servlet we're testing.
		 * As part of the creation process, override some of the
		 * methods so that we can insert pieces of our test
		 * framework instead. 
		 */
		servlet = new AuctionServlet() {
			private MockServletContext servletContext = new MockServletContext();
			private MockAuctionService auctionService = new MockAuctionService();

			/*
			 * Even though the auction service is one of our own
			 * classes, we're not testing it with these tests.
			 * In these tests, we're testing our servlet's behaviour.
			 * So, return a mock auction service that we can 
			 * control absolutely.
			 */
			protected AuctionService getAuctionService() {
				return auctionService;
			}
			
			/*
			 * Remember that we're not testing whether or not
			 * the application server can return the right
			 * servlet context. Other forms of testing will
			 * handle that. We need a mock service context
			 * that we can use to make sure our behaviour
			 * is using it properly.
			 */
			@Override
			public ServletContext getServletContext() {
				return servletContext;
			}
		};
	}

	public void testGetAuctionInformation() throws Exception {
		// mocks request URL http://host:port/root/servlet?id=42
		HttpServletRequest request = new MockHttpServletRequest("GET");
		
		/*
		 * We set up the servlet request with the values we
		 * need for our test. In "real life", the application 
		 * server will set up these values based on user activity.
		 * We can't depend on something as random as user activity
		 * for unit tests, so we just set the values we need. We
		 * can pretty much make the mock request object provide
		 * whatever answers we need to provide our tests with.
		 * 
		 * Remember that we're testing our servlet's response to
		 * these inputs, not the mechanisms that provide them.
		 */
		request.setAttribute("id", "42");
		
		HttpServletResponse response = new MockHttpServletResponse();
		
		/*
		 * Invoke the service method which will--in turn--invoke
		 * the doGet method. This method should set request 
		 * attributes that are forwarded on to a JSP.
		 */
		
		servlet.service(request, response);
		
		assertEquals("Blue Dress", request.getAttribute("name"));
		assertEquals("$42.00", request.getAttribute("price"));
		
		/*
		 * I'm not testing the JSP with this test. I'm just making
		 * sure that the JSP is actually being invoked. We know
		 * that it is if a request for a RequestDispatcher 
		 * "/auction.jsp" has been created.
		 */
		MockServletContext context = (MockServletContext)servlet.getServletContext();
		assertTrue(context.hasRequestDispatcher("/auction.jsp"));
		
		/*
		 * We also expect that the RequestDispatcher has been told
		 * to forward to the JSP. Again, for the purposes of this test,
		 * we don't care if the JSP works or not (that's another
		 * set of tests: possibly--but not necessarily--JUnit tests).
		 */
		MockRequestDispatcher dispatcher = (MockRequestDispatcher) context.getRequestDispatcher("/auction.jsp");
		assertTrue(dispatcher.isForward());
	}

}

Mock objects are created to stand in for objects provided by the application server. Remember that this test is concerned with ensuring that our servlet's code is correct, not the application server's. The mock objects give the servlet exactly what it needs. We have full control of the values provided by the mock objects which means that we can provide consistent (and repeatable) tests. As one of the comments in the code notes, it's dangerous to have tests that depend on user interaction; user interaction is inconsistent and difficult to use in an automated testing framework. Besides, we're not testing that the application server knows how to provide the right values in a request object, we're testing our servlet code.

The servlet looks like this:

package org.eclipse.samples.servlet;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class for Servlet: AuctionServlet
 *
 */
 @SuppressWarnings("serial")
public class AuctionServlet extends javax.servlet.http.HttpServlet implements javax.servlet.Servlet {
 
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		int id = Integer.parseInt((String)request.getAttribute("id"));
		
		AuctionItem item = getAuctionService().getAuctionItem(id);
		
		request.setAttribute("name", item.getName());
		request.setAttribute("price", String.format("$%.2f", item.getPrice()));
		
		getServletContext().getRequestDispatcher("/auction.jsp").forward(request, response);
	}

	protected AuctionService getAuctionService() {
		// TODO Auto-generated method stub
		return null;
	}  	  	  	    
}

Development has just started; we've only implemented one test! The next step is to define more tests and use those tests to drive further development of the servlet.

The mock objects are simple, and are grown as required by the tests (the mock objects are also built test first).

package org.eclipse.servlets.tests.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

public class MockHttpServletRequest implements HttpServletRequest {

	private final String method;
	private Map<String, Object> attributes = new HashMap<String, Object>();

	public MockHttpServletRequest(String method) {
		this.method = method;
	}

	@Override
	public String getMethod() {
		return method;
	}
	
	@Override
	public void setAttribute(String key, Object value) {
		attributes.put(key, value);
	}

	@Override
	public Object getAttribute(String key) {
		return attributes.get(key);
	}
	...
}

Only the methods that have actually been implemented are shown. As more methods are needed by the servlet, they are implemented in the mock objects to make effective testing of the servlet code possible.

Testing Eclipse Plug-ins

TBD

Back to the top