Writing a Unit Test - Identifying Mutators & Accessors
Thomas J. Kennedy
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:
- Mutator - any member function that changes the state of an object.
- Accessor - any member function that examines the state of an object but does not change the state of that object.
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.,
setTitle
- would change the title of some object.getTitle
- would return the title of some object without making any changes,
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:
- function/method interfaces
- unit tests
- function/method implementations
The first step is to design what each function/method will:
- take as input
- generate as output (i.e., return value or side-effect).
- have as a name
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:
- confirming new Recipes can be added.
- making sure insertion order is maintained.
- confirming
Cookbook
state is updated appropriately.
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
-
toString
method inCookbook
. How surprised would you be iftoString
removed a recipe at random, before outputting everything as aString
? -
addRecipe
method inCookbook
. How surprised would you be ifaddRecipe
stored a newRecipe
, and removed another recipe then generated output (usingSystem.out.println
)?
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:
- Identify all mutators.
- Identify all accessors.
- Create a test function for each mutator.
- Within in each test function call (invoke) each accessor.
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.
public Cookbook()
public Cookbook(int r)
public void addRecipe(Recipe toAdd)
public void removeRecipe(int toRemove)
public boolean equals(Object rhs)
public int hashCode()
public Cookbook clone()
public Iterator<Recipe> iterator()
public String toString()
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:
public void addRecipe(Recipe toAdd)
public void removeRecipe(int toRemove)
public Iterator<Recipe> iterator()
public String toString()
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:
public Iterator<Recipe> iterator()
public String toString()
Both methods are accessors:
toString
generates aString
representing aCookbook
iterator
returns anIterator<Recipe>
object that allows us to retrieve eachRecipe
object.We will treat Java Iterators as C++
const_iterator
s.
4.2 Time to Write Some Tests
Now that all functions have been classified, we know that we will have:
- at a minimum, 5 test functions (one for each mutator).
- at a minimum, 4 assertions within each test function (one for each accessor).
- 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 |