1. Introduction
2. Problems
3. Testing in Existing Environments
4. Test Driven Development

1. Introduction

JUnit is a tool initially written by Erich Gamma and Kent Beck . Gamma was one of the co-authors of Design Patterns , collectively known as the "Gang of Four". Beck is the man behind "Extreme Programming" one of the first Agile development methodologies.

2. Problems

I've often heard arguments against the practicality of using JUnit, though in general people do see the advantages. By the way, in my anecdotal experience all of these arguments come from people who are either not using JUnit or use it as a systems test including everything, from the model classes POJOs or EJB's up and including the database.

2.1. JUnit Requires Extra Effort

Writing tests requires writing supplementary code that should cover each class, each method and all code execution paths. So it would be roughly the same size as the real code. Therefore it requires about double the size of code and 200% of the cost of code written without tests.

This reasoning fails on two accounts:

  • It does not require a very substantial extra effort, I would estimate between 10% and 20% of the programming effort, when done right.
  • There are very important benefits, largely exceeding the effort required for writing the tests.

What does require an important extra effort is unit testing tightly coupled code, because you need to set up a lot of 'scaffolding' in order to make the code executable.

The benefits of having unit tests covering a substantial part of the code, are:

  • The risk of introducing faults when changing code, diminishes significantly.
  • Promotes loosely coupled and therefore more maintainable and reusable code.

2.2. Unit Tests require regular maintenance

I see the benefits of unit testing, but I find after a certain time, most tests don't work anymore.

The problem is that you need to run the tests at least once a day. Consider introducing a centralized build system or continuous integration setup where the test are executed automatically every day, from your source code repository.

A number of customers I worked with use Ant scripts in combination with Luntbuild to automatically check-out, compile and run unit tests frequently - at least daily.

Mistakes, regressions, omissions, errors are caught early on, often removing lengthy debug sessions.

3. Testing in Existing Environments

When having a choice, try to use a test driven approach, use IOC or refactor until all dependencies are easily handled in unit tests.

If you do not have that option, because you already have code you need to test but are not allowed to refactor/rewrite or because your colleagues haven't seen the light and keep adding tightly coupled code, then - and only then - use mock objects.

3.1. Test Using Mock Objects

When trying to write unit tests for a particular class, we often find that it depends on other classes and heavy weight technology such as EJB's.

A mock object is an object that can replace that complex runtime dependency by something else that is just as simple as possible.

Mock object - place holder for dependency
Mock object - place holder for dependency

There are a few libraries that provide 'instant' mock objects. We provide you with some easy to understand examples for:

4. Test Driven Development

In an ideal world, you should start by writing a test and then write the code that implements it. This leads to a different

4.1.

4.2. Loosely Coupled

In this section we will look at how some simple structural changes can make code much easier to test.

4.2.1. Structure

The example shown here is a simple system to organize parcels into shipments.

Imagine a company having trucks that drive once a day from a central warehouse (depot1) to different shops (such as depot2).

A typical situation in a 3-tier architecture is that a client (UI tier code) calls a facade/service (Business tier, an EJB, WS, RMI-object,..) that in turn calls a database...

If we want to test this code, we need to get the facade - ParcelServiceImpl1 - up and running, which implies setting up Hibernate and the database behind it.

Sequence diagram for coupled implementation
Sequence diagram for coupled implementation

Some small changes, however, can make a big difference.

In this case we extracted the basic business logic - for this service and method invocation - into its own class ShipmentOrganizer . The result is that we and up with loosely coupled or highly coherent class implementing the business logic.

Instead of having to setup a database and generating data in a known state before testing, we now provide simply a List of Parcels and can test as much scenarios as we like.

Sequence diagram for loosely coupled implementation
Sequence diagram for loosely coupled implementation

Class diagram of both implementations
Class diagram of both implementations

4.2.2. Code

This code is a simplified but working example of a pattern that often occurs in real world systems. In order to run the first implementation, you would have to provide a database and a Hibernate configuration, but that is out of scope for this example.

/**
* This could be a Servlet, a Struts Action,...
*/
public class Client {
	private Depot location;
	private Depot destination;

	public void doGet(HttpServletRequest request, HttpServletResponse response)  {
		ParcelService service = ServiceLocator.getInstance().getService(ParcelService.class);
		List<Shipment> shipments = service.getShipmentsForDestination(location, destination);
		//now print labels, generate a webpage, or do something useful with this information
	}
}
enum Priority {
	LOW, NORMAL, URGENT,
}
import java.util.List;

public interface ParcelService {
	public List<Parcel>t; getParcelsForDestination(Depot location, Depot destination);

	public List<Shipment> getShipmentsForDestination(Depot location, Depot destination);
}
class Shipment {
	private List<Parcel> parcels = new LinkedList<Parcel>();
	private int totalCapacity = 100000;
	private int totalUsed = 0;

	public List<Parcel> getParcels() {
		return Collections.unmodifiableList(parcels);
	}

	public void addParcel(Parcel parcel) {
		totalUsed += parcel.getWeightGrams();
		parcels.add(parcel);
	}

	public int remainingWeightCapacity() {
		return totalCapacity - totalUsed;
	}

	public int getTotalCapacity() {
		return totalCapacity;
	}

	public void setTotalCapacity(int totalCapacity) {
		this.totalCapacity = totalCapacity;
	}

}
class Parcel {
	public static final Comparator<Parcel> PRIORITY_COMPARATOR = new Comparator<Parcel>() {
		@Override
		public int compare(Parcel one, Parcel two) {
			if (one.equals(two)) {
				return 0;
			} else if (two.priority.ordinal() > one.priority.ordinal()) {
				return 1;
			} else {
				return -1;
			}
		}
	};
	private Priority priority = Priority.NORMAL;
	private int weightGrams;
	private String identifier;
	private String description;
	private Depot location;
	private Depot destination;

	public Parcel() {

	}

	public Parcel(Priority priority, int weightGrams, String identifier, String description, Depot location, Depot destination) {
		super();
		this.priority = priority;
		this.weightGrams = weightGrams;
		this.identifier = identifier;
		this.description = description;
		this.location = location;
		this.destination = destination;
	}

	public int getWeightGrams() {
		return weightGrams;
	}

	public void setWeightGrams(int weightGrams) {
		this.weightGrams = weightGrams;
	}

	public String getIdentifier() {
		return identifier;
	}

	public void setIdentifier(String identifier) {
		this.identifier = identifier;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}
}
class Depot {
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}
class ParcelServiceImpl1 implements ParcelService {
	private SessionFactory hibernateSessionFactory;

	@SuppressWarnings("unchecked")
	@Override
	public List<Parcel> getParcelsForDestination(Depot location, Depot destination) {
		Criteria criteria = getHibernateSession().createCriteria(Parcel.class);
		criteria.createAlias("destination", "ddestination")
			.createAlias("location", "dorigin")
			.add(Restrictions.eq("ddestination", destination))
			.add(Restrictions.eq("dorigin", location));
		List<Parcel> parcels = criteria.list();
		return parcels;
	}

	@Override
	public List<Shipment> getShipmentsForDestination(Depot location, Depot destination) {
		List<Parcel> parcels = getParcelsForDestination(location, destination);
		LinkedList<Parcel> sorted = new LinkedList<Parcel>(parcels);
		Collections.sort(sorted, Parcel.PRIORITY_COMPARATOR);
		List<Shipment> shipments = new LinkedList<Shipment>();

		Iterator<Parcel> it = sorted.iterator();
		while (!sorted.isEmpty()) {
			Shipment shipment = new Shipment();
			shipments.add(shipment);
			fillShipment(shipment, sorted);
		}
		return shipments;
	}

	protected Session getHibernateSession() {
		return getSessionFactory().openSession();
	}

	protected SessionFactory getSessionFactory() {
		if (hibernateSessionFactory == null) {
			File file = new File("hibernate.cfg.xml");
			System.out.println(file.getAbsolutePath());
			Configuration configure = new Configuration().configure(file);
			hibernateSessionFactory = configure.buildSessionFactory();
		}
		return hibernateSessionFactory;
	}

	private void fillShipment(Shipment shipment, List<Parcel> sorted) {
		Parcel parcel = null;
		while (null != (parcel = getBiggestWeighingLessThan(shipment.remainingWeightCapacity(), sorted))) {
			sorted.remove(parcel);
			shipment.addParcel(parcel);
		}
	}

	private Parcel getBiggestWeighingLessThan(int weight, List<Parcel> sorted) {
		for (Parcel parcel : sorted) {
			if (parcel.getWeightGrams() < weight) {
				return parcel;
			}
		}
		return null;
	}
}
class ParcelServiceImpl2 implements ParcelService {

	private SessionFactory hibernateSessionFactory;

	@SuppressWarnings("unchecked")
	@Override
	public List<Parcel> getParcelsForDestination(Depot location, Depot destination) {
		Criteria criteria = getHibernateSession().createCriteria(Parcel.class);
		criteria.createAlias("destination", "ddestination").createAlias("location", "dorigin").add(Restrictions.eq("ddestination", destination)).add(
				Restrictions.eq("dorigin", location));
		List<Parcel> parcels = criteria.list();
		return parcels;
	}

	@Override
	public List<Shipment> getShipmentsForDestination(Depot location, Depot destination) {
		List<Parcel> parcels = getParcelsForDestination(location, destination);
		ShipmentOrganizer helper = new ShipmentOrganizer();
		helper.setMaxWeightPerShipment(500000);
		return helper.organizeParcelsInShipments(parcels);
	}

	protected Session getHibernateSession() {
		return getSessionFactory().openSession();
	}

	protected SessionFactory getSessionFactory() {
		if (hibernateSessionFactory == null) {
			File file = new File("hibernate.cfg.xml");
			System.out.println(file.getAbsolutePath());
			Configuration configure = new Configuration().configure(file);
			hibernateSessionFactory = configure.buildSessionFactory();
		}
		return hibernateSessionFactory;
	}
}
class ShipmentOrganizer {
	private int maxWeightPerShipment;

	public int getMaxWeightPerShipment() {
		return maxWeightPerShipment;
	}

	public void setMaxWeightPerShipment(int maxWeightPerShipment) {
		this.maxWeightPerShipment = maxWeightPerShipment;
	}

	public List<Shipment> organizeParcelsInShipments(Collection<Parcel> parcels) {
		LinkedList<Parcel> sorted = new LinkedList<Parcel>(parcels);
		Collections.sort(sorted, Parcel.PRIORITY_COMPARATOR);
		List<Shipment> shipments = new LinkedList<Shipment>();

		Iterator<Parcel> it = sorted.iterator();
		while (!sorted.isEmpty()) {
			Shipment shipment = new Shipment();
			shipment.setTotalCapacity(maxWeightPerShipment);
			shipments.add(shipment);
			fillShipment(shipment, sorted);
		}
		return shipments;
	}

	public void fillShipment(Shipment shipment, List<Parcel> sorted) {
		Parcel parcel = null;
		while (null != (parcel = getFirstWeighingLessThan(shipment.remainingWeightCapacity(), sorted))) {
			sorted.remove(parcel);
			shipment.addParcel(parcel);
		}
	}

	public Parcel getFirstWeighingLessThan(int weight, List<Parcel> sorted) {
		for (Parcel parcel : sorted) {
			if (parcel.getWeightGrams() < weight) {
				return parcel;
			}
		}
		return null;
	}
}
class ServiceLocator {
	public <T> T getService(Class<T> type) {
		return new ParcelServcieImpl1();
	}

	public static ServiceLocator getInstance() {
		return new ServiceLocator();
	}
}
import static org.junit.Assert.assertEquals;

public class UTestShipmentOrganizer {
	private ShipmentOrganizer instance;
	private List<Parcel> parcels;
	private Depot main;
	private Depot branch1;
	private Depot branch2;

	@Before
	public void setUp() throws Exception {
		instance = new ShipmentOrganizer();
		main = new Depot();
		branch1 = new Depot();
		branch2 = new Depot();
	}

	@Test
	public void testGetFirstWeighingLessThan() {
		parcels = Arrays.asList(new Parcel[] { 
			new Parcel(Priority.URGENT, 15000, "001", "Description 1", main, branch1),
			new Parcel(Priority.URGENT, 10000, "002", "Description 2", main, branch1),
			new Parcel(Priority.NORMAL, 15000, "003", "Description 3", main, branch1),
			new Parcel(Priority.LOW, 7000, "004", "Description 4", main, branch1) 
			});
		instance.setMaxWeightPerShipment(100000);
		assertEquals("004", instance.getFirstWeighingLessThan(9000, parcels).getIdentifier());
		assertEquals("001", instance.getFirstWeighingLessThan(16000, parcels).getIdentifier());
		assertEquals("002", instance.getFirstWeighingLessThan(15000, parcels).getIdentifier());
		assertNull(instance.getFirstWeighingLessThan(6000, parcels));
	}
}
		 
public class UTestShipmentOrganizer {
	//setup a database ...
	//run into problems...
	//ask someone...
	//most give up here.
}