Test Data Builder

One of the more time-consuming and tedious chores in writing unit tests that involve mocks, is setting up the test data for mock expectations. For example, let's say we are testing service class code like this:

List<Student> advisees = advisorDao.getAdvisees();

Our unit test would use a mock version of advisorDao, and we would tell the mock framework to expect a call to getAdvisees() and for this method call to return a list of Student objects, like so:

expect(advisorDao.getAdvisees()).andReturn(studentList);

We would need to manually create the studentList list, instantiate a number of Student objects, and add them all to the list. Perhaps our test also requires us to peek inside the Student objects and examine the enrollment profile or other embedded objects. We would be required to instantiate all of these objects and set them inside our Student objects. This takes a lot of time and is very tedious.

The intent of the test data builder framework is to make "helper" classes available that will do a lot of this data setup for the programmer. These will be simple factory classes, instantiating commonly used objects like Student, and populating them with values that make sense.

Design considerations:

  • Make the framework simple and easy to use
  • Keep the interfaces uncluttered
  • Provide the basics that should be good enough for most tests, but allow the flexibility for unit tests to tweak the data if necessary.

Here is an example of one of these builder classes - it's called CoreDataFactory and is intended to provide common data used across many apps. The interfaces for creating Student objects is like this:

public Student buildStudent();

public Student buildStudent(String termCode);

There are just two variations- the first (no-arg) method will return a Student object with all basic data fields filled in. It will also populate enrollment profile information for the current term (based on a simple current date calculation). The second method, taking term code as an argument builds a similar Student object, but it uses the supplied term code for any term-specific data like enrollment profile.

The builder methods will randomly generate values for the student name, mit id, pidm, kerb name, etc, so that each student object returned by the builder methods will be different.

So a unit test needing Student objects for test data now only needs to do this for each student object:

Student student = builder.buildStudent();

and no longer needs to do this:

PersonName personName = new PersonName("David", "Lee", "Roth");
String kerbName = "dlr";
String emailAddress = kerbName + "@mit.edu";

int pidm = 12212871;
int mitId = 90002312;

Student student = new Student(String.valueOf(pidm), String.valueOf(mitId), kerbName,
                              personName, emailAddress);
Calendar now = Calendar.getInstance();

student.setCitizenship(new Citizenship("US", "United States"));
student.setEthnicity(new StudentEthnicity());
Calendar birthDate = Calendar.getInstance();
birthDate.set(Calendar.YEAR, birthDate.get(Calendar.YEAR) - 20);  // Born 20 years ago
student.setBirthDate(birthDate.getTime());
student.setDeceased(false);
student.setGender(Gender.FEMALE);
student.setStudentHolds(new ArrayList<StudentHold>());
student.setVersion(1l);
student.setCreateBy("testuser");
student.setCreateDate(now.getTime());
student.setModifyBy("testuser2");
student.setModifyDate(now.getTime());

 // Enrollment profile:
Set enrollProfiles = new HashSet();

// ... more code that builds enrollment profile objects...

student.setStudentEnrollProfiles(enrollProfiles);

Test Builder Package / Module Location

I originally thought that the test builder factory classes would go into the csf-test module, as they are intended to be helper classes for unit tests. However, because the builder classes use the domain classes, this setup would cause circular dependencies - for example, csf-common-legacy would depend on csf-test, and csf-test would depend on csf-common-legacy. Because of this, it seems to make sense to place the builder classes close to the domain objects they are building. So the CoreDataFactory could go into csf-common-legacy in a new package in the domain area: edu.mit.common.domain.builders

A different way of looking at this is that by creating the helper classes, we are just abstracting out duplicate code from the unit test classes themselves. So perhaps the builders do belong in the test hierarchy. They are not strictly unit tests themselves, but by putting them under test, they would only be used when tests were run and would not be deployed as part of an app.

Core Data Factory Javadoc

Here's a screenshot showing the Javadoc for the Core Data Factory class.

Does the Core Data Factory Satisfy the Goals?

Here's how I think the factory prototype satisfies the goals:

  • Make the framework simple and easy to use

Once you have an instance of CoreDataFactory, getting a stucb Student object is a single method call.

  • Keep the interfaces uncluttered

Taking student as an example, there are two build options: one that takes no arguments, one that takes a single termCode argument. We could have created a longer arg list for finer grained customization, but it seems better to keep the interface short and simple. If a unit test needs to tweak the data, it can do that after getting the student object.

  • Provide the basics that should be good enough for most tests, but allow the flexibility for unit tests to tweak the data if necessary.
  • No labels