Writing a Unit Test - Identifying Mutators & Accessors

Thomas J. Kennedy

Contents:

In CS 330 I use a Cookbook class as an example in a few discussions. For this example, we will use the final version of the class from Using the Java Class Checklist.

1 Getting Started

When working with member functions (i.e., functions that are part of a class), two terms must be kept in mind:

By definition constructors are mutators. Each constructor creates an initial object. The terms getter and setter refer to member functions (methods) that work with (public, private, or protected) member data, e.g.,

To write good unit tests, the result(s) of calling each mutator must be fully examined, using the available accessors. In this course… we will follow the mutator-accessor strategy.

2 An Example Cookbook Class

Let us work with an example Cookbook class. Note that I have been fairly cavalier with a few parts of the Javadoc documentation.

Example 1: Cookbook Class Interface
/**
 * A collection of Recipes.
 */
public class Cookbook
    implements Cloneable, Iterable<Recipe>
{
    /**
     * Create a Cookbook that can contain at most MAX_RECIPES
     * recipes
     */
    public Cookbook()
    {
        //...
    }

    /**
     * Create a Cookbook that can contain at most _r_
     * recipes
     */
    public Cookbook(int r)
    {
        //...
    }

    /**
     * Add a Recipe
     *
     * @param toAdd new Recipe
     */
    public void addRecipe(Recipe toAdd)
    {
        //...
    }

    /**
     * Remove a Recipe
     *
     * @param toRemove index of Recipe to remove
     */
    public void removeRecipe(int toRemove)
    {
        //...
    }

    /**
     * Compare two recipes based on the recipes they contain, ignoring the
     * the order of the recipes.
     *
     * @param rhs object against which to compare
     *
     * @return true if both this and rhs are Cookbooks with the same recipes
     */
    @Override
    public boolean equals(Object rhs)
    {
        //...
    }

    /**
     * Compute the hashcode by adding all recipe hashcodes together.
     *
     * @return integer hashcode
     */
    @Override
    public int hashCode()
    {
        //...
    }

    /**
     * Create a deep copy.
     */
    @Override
    public Cookbook clone()
    {
        //...
    }

    /**
     * This depends on implementation decisions (e.g., selected data structure).
     */
    @Override
    public Iterator<Recipe> iterator()
    {
        //...
    }

    /**
     * List each Recipe, seperating them with a blank line followed by "---"
     * and a second blank line.
     */
    @Override
    public String toString()
    {
        //...
    }
}

Take note of the member functions. There are no implementations/definitions listed. When following proper Test Driven Development (TDD), code is written in the following order:

  1. function/method interfaces
  2. unit tests
  3. function/method implementations

The first step is to design what each function/method will:

When writing tests, we are not concerned with the how. We are concerned with the what. Take as an example the addRecipe method. What data structure is being used? Is it a List, a Vector, a LinkedHashSet, or an array? The data structure does not matter for the test. We are only concerned with:

3 What Does Appropriate Mean?

What does the phrase updated appropriately mean? The term appropriate is nebulous. In this context… the phrase updated appropriately can be answered by using two questions:

Example 2: Unit Test Questions
/**
 * 1 - Does this piece of code perform the operations
 *     it was designed to perform?
 *
 * 2 - Does this piece of code do something it was not
 *     designed to perform?
 *
 * 1 Test per mutator
 */

Why did I write these questions as a multiline Java comment? Anytime I am writing unit tests, I copy-and-paste that exact comment at the top of my test code.

Consider the

Now… we are getting dangerously close to testing for failures. We are not writing test code to check for failures. We are writing test code to confirm correct behavior (which is a subtle distinction).

4 The Mutator-Accessor Strategy

The mutator-accessor strategy is a method of writing unit test suites for object-oriented code. The methodology is:

  1. Identify all mutators.
  2. Identify all accessors.
  3. Create a test function for each mutator.
  4. Within in each test function call (invoke) each accessor.

You will see a few examples of this process in my recorded JUnit lectures.

4.1 Applying the Strategy to Cookbook

Take another look at the Cookbook class. We need to classify each method as either an accessor or mutator.

Did you notice how these are all public member functions? We are writing tests using the public interface of Cookbook. Testing private/protected member functions is a debated issue. For now… we will treat private/protected member functions as implementation details (i.e., part of the how).

Let us create a table.

Mutators Accessors
Cookbook() equals()
Cookbook(int r) } hashCode()
clone

The first four entries are fairly easy (compared to the rest of the methods). Constructors create objects. Both equals and hashCode are comparison functions. We are left with four methods to classify:

If we take a look at the names of the next two functions, addRecipe and removeRecipe, along with their documentation, we know that they store a Recipe or remove a Recipe, respectively.

Mutators Accessors
Cookbook() equals()
Cookbook(int r) hashCode()
clone
addRecipe
removeRecipe

That leaves us with two methods:

Both methods are accessors:

4.2 Time to Write Some Tests

Now that all functions have been classified, we know that we will have:

  1. at a minimum, 5 test functions (one for each mutator).
  2. at a minimum, 4 assertions within each test function (one for each accessor).
  3. at a minimum, 20 (5 times 4) assertions in the entire test suite.
Mutators Accessors
Cookbook() equals()
Cookbook(int r) hashCode()
clone
addRecipe iterator
removeRecipe toString