Testing ADTs

Steven J Zeil

Last modified: Sep 14, 2017
Contents:

Abstract

It would be nice if every new ADT we wrote worked correctly the first time it compiled properly, but the real world just doesn’t work that way.

One advantage of organizing your code into a collection of ADTs is that ADTs provide, not only a convenient way to package up the functionality of your code, but also a convenient basis for testing.

The *Unit test frameworks give us a powerful tool for unit testing. But we still need to pick the tests.

In this lesson we look at how to use a unit test framework to “thoroughly” test an ADT. Our approach is based upon dividing the ADT interface into

Our approach then becomes:

  1. Each test case explores one mutator, and each mutator has at least one test case.
  2. In each test case, assert the effect of that mutator as revealed through each accessor.

1 Be Smart: Generate and Check

Testing addContact

In our earlier test for addContact, we weren’t particularly thorough:

TEST_F (MailingListTests, addContact) {
  mlist.addContact (jones);
  EXPECT_TRUE (mlist.contains("Jones"));
  EXPECT_EQ ("Jones", mlist.getContact("Jones").getName());
  EXPECT_EQ (4, ml.size());
}

Adding Variety

Some of our concerns could be addressed by adding tests but varying the parameters:

TEST_F (MailingListTests, addExistingContact) {
  mlist.addContact (baker);
  EXPECT_TRUE (mlist.contains("Baker"));
  EXPECT_EQ ("Baker", mlist.getContact("Baker").getName());
  EXPECT_EQ (3, ml.size());
}

1.1 Generate and Check

A useful design pattern for producing well-varied tests is

for v: varieties of ADT {
   ADT x = generate(v);
   result = applyFunctionBeingTested(x);
   check (x, result); 
}
   ADT x (constructor1);
   result = applyFunctionBeingTested(x);
   check (x, result); 

   ADT x2 (constructor2,  ... );
   result = applyFunctionBeingTested(x2);
   check (x2, result); 


Example: addContact

A more elaborate fixture will aid in generating mailing lists of different sizes:

fixture.cpp
// The fixture for testing class MailingList.
class MailingListTests  {
public:
  Contact jones;
  vector<Contact> contacts;
  
  MailingListTests() {
    jones = Contact("Jones", "21 Penn. Ave.");
    contacts.clear();
    contacts.push_back (Contact ("Baker", "Drury Ln."));
    contacts.push_back (Contact ("Holmes", "221B Baker St."));
    contacts.push_back(jones);
    contacts.push_back (Contact ("Wolfe", "454 W. 35th St."));
  }

  ~MailingListTests() {
  }


  MailingList generate(int n) const
  {
    MailingList m;
    for (int i = 0; i < n; ++i)
      m.addContact(contacts[i]);
    return m;
  }

};

Example: addContact - many sizes

testAdd1.cpp
BOOST_FIXTURE_TEST_CASE (addContact, MailingListTests) {
  for (unsigned sz = 0; sz < contacts.size(); ++sz)
    {
      MailingList ml0 = generate(sz);
      MailingList ml (ml0);
      bool alreadyContained = ml.contains("Jones");
      ml.addContact (jones);
      BOOST_CHECK (ml.contains("Jones"));
      BOOST_CHECK_EQUAL ("Jones", ml.getContact("Jones").getName());
      if (alreadyContained)
          BOOST_CHECK_EQUAL (ml.size(), sz);
      else
          BOOST_CHECK_EQUAL (ml.size(), sz+1);
    }
}

Example: addContact - ordering

testAdd2.cpp
BOOST_FIXTURE_TEST_CASE (addContact, MailingListTests) {
  for (unsigned sel = 0; sel < contacts.size(); ++sel)
    {
      Contact& toAdd = contacts[sel];
      const Name& nameToAdd = contacts[sel];
      for (unsigned sz = 0; sz < contacts.size(); ++sz)
        {
          MailingList ml0 = generate(sz);
          MailingList ml (ml0);
          bool alreadyContained = ml.contains(nameToAdd);
          ml.addContact (toAdd);
          BOOST_CHECK (ml.contains(nameToAdd));
          BOOST_CHECK_EQUAL (nameToAdd, ml.getContact(nameToAdd).getName());
          if (alreadyContained)
            BOOST_CHECK_EQUAL (ml.size(), sz);
          else
            BOOST_CHECK_EQUAL (ml.size(), sz+1);
        }
    }
}

2 Be Thorough: Mutators and Accessors

OK, let’s say we want to design a unit test suite for our MailingList ADT.

Just staring at the ADT interface, where do we start? The criteria we suggested earlier (typical values, extremal values, special values) may be of help, but it’s far from obvious how to apply those ideas.

There is an organized way to approach this test design. It starts with the recognition that the member functions of an ADT can usually be divided into two groups - the mutators and the accessors.

Mutator functions are the functions that alter the value of an object. These include constructors and assignment operators.

Accessor functions are the functions that “look at” the value of an object but do not change it. Some functions may both alter an object and return part of its value - we’ll have to wing it a bit with those.


Mutators and Accessors

To test an ADT, divide the public interface into


Organizing ADT Tests

The basic procedure for writing an ADT unit test is to

  1. Consider each mutator in turn.

  2. Write a test that begins by applying that mutator function.

  3. Then consider how that mutator will have affected the results of each accessor.

  4. Write assertions to test those effects.

Commonly, each mutator will be tested in a separate function.

2.1 Example: Unit Testing of the MailingList Class

mailinglist.h
#ifndef MAILINGLIST_H
#define MAILINGLIST_H

#include <iostream>
#include <string>

#include "contact.h"

/**
   A collection of names and addresses
*/
class MailingList
{
public:
  MailingList();
  MailingList(const MailingList&);
  ~MailingList();

  const MailingList& operator= (const MailingList&);

  // Add a new contact to the list
  void addContact (const Contact& contact);

  // Does the list contain this person?
  bool contains (const Name&) const;

  // Find the contact
  const Contact& getContact (const Name& nm) const;
  //pre: contains(nm)

  // Remove one matching contact
  void removeContact (const Contact&);
  void removeContact (const Name&);

  // combine two mailing lists
  void merge (const MailingList& otherList);

  // How many contacts in list?
  int size() const;


  bool operator== (const MailingList& right) const;
  bool operator< (const MailingList& right) const;

private:

  struct ML_Node {
    Contact contact;
    ML_Node* next;

    ML_Node (const Contact& c, ML_Node* nxt)
      : contact(c), next(nxt)
    {}
  };

  ML_Node* first;
  ML_Node* last;
  int theSize;

  // helper functions
  void clear();
  void remove (ML_Node* previous, ML_Node* current);

  friend std::ostream& operator<< (std::ostream& out, const MailingList& addr);
};

// print list, sorted by Contact
std::ostream& operator<< (std::ostream& out, const MailingList& list);


#endif

Look at our MailingList class.

Question: What are the mutators? What are the accessors?

Answer:

Unit Test Skeleton

Here is the basic skeleton for our test suite.

skeleton.cpp
#define BOOST_TEST_MODULE MailingList test

#include "mailinglist.h"

#include <string>
#include <sstream>
#include <vector>

#include <boost/test/included/unit_test.hpp>

using namespace std;

Now we start going through the mutators.

2.1.1 Testing the Constructors

The first mutators we listed were the two constructors. Let’s start with the simpler of these.


Apply The Mutator

BOOST_AUTO_TEST_CASE ( constructor ) {
    MailingList ml;
      ⋮

First we start by applying the mutator.


Apply the Accessors to the Mutated Object

Then we go down the list of accessors and ask what we expect each one to return.

BOOST_AUTO_TEST_CASE ( constructor ) {
    MailingList ml;
    BOOST_CHECK_EQUAL (0, ml.size());
    BOOST_CHECK (!ml.contains("Jones"));
    BOOST_CHECK_EQUAL (ml, MailingList());
    BOOST_CHECK (!(ml < MailingList()));
}

  1. It’s pretty clear, for example, that the size() of the list will be 0
  2. contains() would always return false
  3. The EQUAL test checks the operator==
  4. We check for consistency of operator<

We can’t check the accessors


Testing the Copy Constructor

testCons1.cpp
BOOST_FIXTURE_TEST_CASE ( copyConstructor,  MailingListTests) {
  for (unsigned sz = 0; sz < contacts.size(); ++sz)
    {
      MailingList ml0 = generate(sz);             ➀
      MailingList ml1 = ml0; // copy constructor  ➁
      shouldBeEqual(ml0, ml1);                    ➂
    }
  {
    // Minimal check for deep copy - changing one should not affect the other
    MailingList ml0 = generate(2);
    MailingList ml1 = ml0; // copy constructor
    ml1.addContact(jones);                        ➃
    BOOST_CHECK_EQUAL (2, ml0.size());
    BOOST_CHECK_NE (ml0, ml1);
  }
}

The Check Function for Copying

shouldBeEqual.cpp
// The fixture for testing class MailingList.
class MailingListTests  {
 public:
  Contact jones;
  vector<Contact> contacts;
  
  MailingListTests() {
    ⋮
  
  void shouldBeEqual (const MailingList& ml0, const MailingList& ml1) const
  {
    BOOST_CHECK_EQUAL (ml1.size(), ml0.size());  ➀
    for (int i = 0; i < ml0.size(); ++i)         ➁
      {
	BOOST_CHECK_EQUAL(ml1.contains(contacts[i].getName()), ml0.contains(contacts[i].getName()));
	if (ml1.contains(contacts[i].getName()))
	  BOOST_CHECK_EQUAL(ml1.getContact(contacts[i].getName()), ml0.getContact(contacts[i].getName()));
      }
    
    BOOST_CHECK_EQUAL (ml0, ml1);    ➂
    BOOST_CHECK (!(ml0 < ml1));
    BOOST_CHECK (!(ml1 < ml0));
    
    ostringstream out0;              ➂
    out0 << ml0;
    ostringstream out1;
    out1 << ml1;
    
    BOOST_CHECK_EQUAL (out0.str(), out1.str());
    
  }
  
};

2.1.2 Testing the Assignment Operator

testAsst.cpp
BOOST_FIXTURE_TEST_CASE ( assignment,  MailingListTests) {
  for (unsigned sz = 0; sz < contacts.size(); ++sz)
    {
      MailingList ml0 = generate(sz);
      MailingList ml1;
      MailingList ml2 = (ml1 = ml0); // assignment   ➀
      shouldBeEqual(ml0, ml1);                       ➁
      shouldBeEqual(ml0, ml2);  // assignment returns a value
    }
  {
    // Minimal check for deep copy - changing one should not affect the other
    MailingList ml0 = generate(2);
    MailingList ml1;
    ml1 = ml0; // copy constructor
    ml1.addContact(jones);
    BOOST_CHECK_EQUAL (2, ml0.size());
    BOOST_CHECK_NE (ml0, ml1);
  }
}

2.1.3 Testing addContact

testAdd.cpp
BOOST_FIXTURE_TEST_CASE (addContact, MailingListTests) {
  for (unsigned sel = 0; sel < contacts.size(); ++sel)
    {
      Contact& toAdd = contacts[sel];
      const Name& nameToAdd = toAdd.getName();
      for (unsigned sz = 0; sz < contacts.size(); ++sz)
        {
          MailingList ml0 = generate(sz);
          MailingList ml (ml0);
          bool alreadyContained = ml.contains(nameToAdd);
          ml.addContact (toAdd);
          BOOST_CHECK (ml.contains(nameToAdd));
          BOOST_CHECK_EQUAL (toAdd, ml.getContact(nameToAdd));
          if (alreadyContained)
            {
              BOOST_CHECK_EQUAL (ml.size(), (int)sz);
              BOOST_CHECK_EQUAL (ml0, ml);
              BOOST_CHECK (!(ml0 < ml));
              BOOST_CHECK (!(ml < ml0));
            }
          else
            {
              BOOST_CHECK_EQUAL (ml.size(), (int)sz+1);
              BOOST_CHECK_NE (ml0, ml);
              BOOST_CHECK ((ml0 < ml) || (ml < ml0));    // one must be true
              BOOST_CHECK (!((ml0 < ml) && (ml < ml0))); // ...but not both
            }
          ostringstream out;
          out << ml;
          BOOST_CHECK_NE (out.str().find(nameToAdd), string::npos);
        }
    }
}

2.1.4 And so on…

We continue in this manner until done.

fullTest.cpp
#define BOOST_TEST_MODULE MailingList test

#include "mailinglist.h"

#include <string>
#include <sstream>
#include <vector>

#include <boost/test/included/unit_test.hpp>
//#define  BOOST_TEST_DYN_LINK 1
//#include <boost/test/unit_test.hpp>

using namespace std;

// The fixture for testing class MailingList.
class MailingListTests  {
 public:
  Contact jones;
  vector<Contact> contacts;
  
  MailingListTests() {
    jones = Contact("Jones", "21 Penn. Ave.");
    contacts.clear();
    contacts.push_back (Contact ("Muffin Man", "Drury Ln."));
    contacts.push_back (Contact ("Holmes", "221B Baker St."));
    contacts.push_back(jones);
    contacts.push_back (Contact ("Wolfe", "454 W. 35th St."));
  }
  
  ~MailingListTests() {
  }
  
  
  MailingList generate(int n) const
  {
    MailingList m;
    for (int i = 0; i < n; ++i)
      m.addContact(contacts[i]);
    return m;
  }
  
  void shouldBeEqual (const MailingList& ml0, const MailingList& ml1) const
  {
    BOOST_CHECK_EQUAL (ml1.size(), ml0.size());
    for (int i = 0; i < ml0.size(); ++i)
      {
        BOOST_CHECK_EQUAL(ml1.contains(contacts[i].getName()), ml0.contains(contacts[i].getName()));
        if (ml1.contains(contacts[i].getName()))
          BOOST_CHECK_EQUAL(ml1.getContact(contacts[i].getName()), ml0.getContact(contacts[i].getName()));
      }
    
    BOOST_CHECK_EQUAL (ml0, ml1);
    BOOST_CHECK (!(ml0 < ml1));
    BOOST_CHECK (!(ml1 < ml0));
    
    ostringstream out0;
    out0 << ml0;
    ostringstream out1;
    out1 << ml1;
    
    BOOST_CHECK_EQUAL (out0.str(), out1.str());
    
  }
  
};

BOOST_AUTO_TEST_CASE ( constructor ) {
  MailingList ml;
  BOOST_CHECK_EQUAL (0, ml.size());
  BOOST_CHECK (!ml.contains("Jones"));
  BOOST_CHECK_EQUAL (ml, MailingList());
  BOOST_CHECK (!(ml < MailingList()));
}


BOOST_FIXTURE_TEST_CASE ( copyConstructor,  MailingListTests) {
  for (unsigned sz = 0; sz < contacts.size(); ++sz)
    {
      MailingList ml0 = generate(sz);
      MailingList ml1 = ml0; // copy constructor
      shouldBeEqual(ml0, ml1);
    }
  {
    // Minimal check for deep copy - changing one should not affect the other
    MailingList ml0 = generate(2);
    MailingList ml1 = ml0; // copy constructor
    ml1.addContact(jones);
    BOOST_CHECK_EQUAL (2, ml0.size());
    BOOST_CHECK_NE (ml0, ml1);
  }
}

BOOST_FIXTURE_TEST_CASE ( assignment,  MailingListTests) {
  for (unsigned sz = 0; sz < contacts.size(); ++sz)
    {
      MailingList ml0 = generate(sz);
      MailingList ml1;
      MailingList ml2 = (ml1 = ml0); // assignment
      shouldBeEqual(ml0, ml1);
      shouldBeEqual(ml0, ml2);  // assignment returns a value
    }
  {
    // Minimal check for deep copy - changing one should not affect the other
    MailingList ml0 = generate(2);
    MailingList ml1;
    ml1 = ml0; // copy constructor
    ml1.addContact(jones);
    BOOST_CHECK_EQUAL (2, ml0.size());
    BOOST_CHECK_NE (ml0, ml1);
  }
}


BOOST_FIXTURE_TEST_CASE (addContact, MailingListTests) {
  for (unsigned sel = 0; sel < contacts.size(); ++sel)
    {
      Contact& toAdd = contacts[sel];
      const Name& nameToAdd = toAdd.getName();
      for (unsigned sz = 0; sz < contacts.size(); ++sz)
        {
          MailingList ml0 = generate(sz);
          MailingList ml (ml0);
          bool alreadyContained = ml.contains(nameToAdd);
          ml.addContact (toAdd);
          BOOST_CHECK (ml.contains(nameToAdd));
          BOOST_CHECK_EQUAL (toAdd, ml.getContact(nameToAdd));
          if (alreadyContained)
            {
              BOOST_CHECK_EQUAL (ml.size(), (int)sz);
              BOOST_CHECK_EQUAL (ml0, ml);
              BOOST_CHECK (!(ml0 < ml));
              BOOST_CHECK (!(ml < ml0));
            }
          else
            {
              BOOST_CHECK_EQUAL (ml.size(), (int)sz+1);
              BOOST_CHECK_NE (ml0, ml);
              BOOST_CHECK ((ml0 < ml) || (ml < ml0));    // one must be true
              BOOST_CHECK (!((ml0 < ml) && (ml < ml0))); // ...but not both
            }
          ostringstream out;
          out << ml;
          BOOST_CHECK_NE (out.str().find(nameToAdd), string::npos);
        }
    }
}

BOOST_FIXTURE_TEST_CASE (removeContact, MailingListTests) {
  for (unsigned sel = 0; sel < contacts.size(); ++sel)
    {
      Contact& toAdd = contacts[sel];
      const Name& nameToAdd = toAdd.getName();
      for (unsigned sz = 0; sz < contacts.size(); ++sz)
        {
          MailingList ml0 = generate(sz);
          MailingList ml (ml0);
          bool alreadyContained = ml.contains(nameToAdd);
          ml.removeContact (toAdd);
          BOOST_CHECK (!ml.contains(nameToAdd));
          if (!alreadyContained)
            {
              BOOST_CHECK_EQUAL (ml.size(), (int)sz);
              BOOST_CHECK_EQUAL (ml0, ml);
              BOOST_CHECK (!(ml0 < ml));
              BOOST_CHECK (!(ml < ml0));
            }
          else
            {
              BOOST_CHECK_EQUAL (ml.size(), (int)sz-1);
              BOOST_CHECK_NE (ml0, ml);
              BOOST_CHECK ((ml0 < ml) || (ml < ml0));    // one must be true
              BOOST_CHECK (!((ml0 < ml) && (ml < ml0))); // ...but not both
            }
          ostringstream out;
          out << ml;
          string outs = out.str();
          BOOST_CHECK_EQUAL (outs.find(nameToAdd), string::npos);
        }
    }
}

BOOST_FIXTURE_TEST_CASE (removeContactByName, MailingListTests) {
  for (unsigned sel = 0; sel < contacts.size(); ++sel)
    {
      Contact& toAdd = contacts[sel];
      const Name& nameToAdd = toAdd.getName();
      for (unsigned sz = 0; sz < contacts.size(); ++sz)
        {
          MailingList ml0 = generate(sz);
          MailingList ml (ml0);
          bool alreadyContained = ml.contains(nameToAdd);
          ml.removeContact (nameToAdd);
          BOOST_CHECK (!ml.contains(nameToAdd));
          if (!alreadyContained)
            {
              BOOST_CHECK_EQUAL (ml.size(), (int)sz);
              BOOST_CHECK_EQUAL (ml0, ml);
              BOOST_CHECK (!(ml0 < ml));
              BOOST_CHECK (!(ml < ml0));
            }
          else
            {
              BOOST_CHECK_EQUAL (ml.size(), (int)sz-1);
              BOOST_CHECK_NE (ml0, ml);
              BOOST_CHECK ((ml0 < ml) || (ml < ml0));    // one must be true
              BOOST_CHECK (!((ml0 < ml) && (ml < ml0))); // ...but not both
            }
          ostringstream out;
          out << ml;
          BOOST_CHECK_EQUAL (out.str().find(nameToAdd), string::npos);
        }
    }
}

BOOST_FIXTURE_TEST_CASE ( merging,  MailingListTests) {
  
  MailingList ml0;
  ml0.addContact(contacts[0]);
  ml0.addContact(contacts[1]);
  ml0.addContact(contacts[2]);
  
  MailingList ml1;
  ml1.addContact(contacts[2]);
  ml1.addContact(contacts[3]);
  
  ml1.merge(ml0);
  shouldBeEqual(ml1, generate(4));
}


2.2 Testing for Pointer/Memory Faults

In our example, we did not write explicit tests for the destructor. Partly that’s because the destructor is already being invoked a lot - every time we exit a function or a {} statement block that declares a local variable. So we have some good reason to hope that the destructor has been well exercised already.

Also, most of the effects of a destructor are hard to see directly. How do you check to see if memory has been successfully deleted?

Most destructors simply delete pointers, and pointer issues are particularly difficult to test and debug.


Tools for Testing Pointer/Memory Faults

2.3 Case Study 1: Unit Test of NumericLiteral

One of the stories in our spreadsheet example involves numeric literals, the component of an expression that represents a constant numeric value like “3.14159” or “42”.

As an API user I would like to add a numeric literal to a cell in a spreadsheet.

Numeric literals are a kind of Expression, a central idea in the spreadsheet. This story offers an opportunity for a look at a realistic set of tests.

2.3.1 Expressions

Expression.java
package edu.odu.cs.espreadsheet.expressions;

import java.io.StringReader;

import edu.odu.cs.espreadsheet.ExpressionParseError;
import edu.odu.cs.espreadsheet.Spreadsheet;
import edu.odu.cs.espreadsheet.values.Value;


/**
 * Expressions can be thought of as trees.  Each non-leaf node of the tree
 * contains an operator, and the children of that node are the subexpressions
 * (operands) that the operator operates upon.  Constants, cell references,
 * and the like form the leaves of the tree.
 * 
 * For example, the expression (a2 + 2) * c26 is equivalent to the tree:
 * 
 *              *
 *             / \
 *            +   c26
 *           / \
 *         a2   2
 * 
 * @author zeil
 *
 */
public abstract class Expression implements Cloneable
{

        /**
         *  How many operands does this expression node have?
         *  
         * @return # of operands required by this operator
         */
        public abstract int arity();

        /**
         *  Get the k_th operand
         * @param k operand number
         * @return k_th operand if 0 < k < arity()
         * @throws IndexOutOfBoundsException if k is outside of those bounds
         */
        public abstract Expression operand(int k) throws IndexOutOfBoundsException;


        /**
         *  Evaluate this expression, using the provided spreadsheet to resolve 
         *  any cell referneces.
         *  
         * @param usingSheet spreadsheet form which to obtain values of 
         * cells referenced by this expression
         * 
         * @return value of the expression or null if the cell is empty
         */
        public abstract Value evaluate(Spreadsheet usingSheet);





        /**
         * Copy this expression (deep copy), altering any cell references
         * by the indicated offsets except where the row or column is "fixed"
         * by a preceding $. E.g., if e is  2*D4+C$2/$A$1, then
         * e.copy(1,2) is 2*E6+D$2/$A$1, e.copy(-1,4) is 2*C8+B$2/$A$1
         * 
         * @param colOffset number of columns to offset this copy
         * @param rowOffset number of rows to offset this copy
         * @return a copy of this expression, suitable for placing into 
         *           a cell (ColOffSet,rowOffset) away from its current position.
         *             
         */
        public abstract Expression clone (int colOffset, int rowOffset);

        /**
         * Copy this expression.
         */
        @Override
        public Expression clone ()
        {
                return clone(0,0);
        }


        /**
         * Attempt to convert the given string into an expression.
         * @param in
         * @return
         */
        public static Expression parse (String in) throws ExpressionParseError
        {
                try {
                        parser p = new parser(new ExpressionScanner(new StringReader(in)));
                    Expression expr = (Expression)p.parse().value;
                    return expr;
                } catch (Exception ex) {
                        throw new ExpressionParseError("Cannnot parse " + in);
                }
        }


        @Override
        public String toString ()
        {
	    ⋮
        }


        @Override
        public abstract boolean equals (Object obj);
        
        @Override
        public abstract int hashCode ();
        
        

        // The following control how the expression gets printed by 
        // the default implementation of toString

        /**
         * If true, print in inline form.
         * If false, print as functionName(comma-separated-list).
         * 
         * @return indication of whether to print in inline form.
         * 
         */
        public abstract boolean isInline();

        /**
         * Parentheses are placed around an expression whenever its precedence
         * is lower than the precedence of an operator (expression) applied to it.
         * E.g., * has higher precedence than +, so we print 3*(a1+1) but not
         * (3*a1)+1
         * 
         * @return precedence of this operator
         */
        public abstract int precedence();


        /**
         * Returns the name of the operator for printing purposes.
         * For constants/literals, this is the string version of the constant value.
         * 
         * @return the name of the operator for printing purposes.
         */
        public abstract String getOperator();



}

A lot of this design was lifted from an earlier project of mine.

2.3.2 Numeric Literals

The NumericLiteral class is a subclass of Expression and needs to supply function bodies for all of the unimplemented functions declared in Expression.

NumericLiteral.java
package edu.odu.cs.espreadsheet.expressions;

import edu.odu.cs.espreadsheet.Spreadsheet;
import edu.odu.cs.espreadsheet.values.NumericValue;
import edu.odu.cs.espreadsheet.values.Value;

/**
 * This class represents numeric constants appearing within an expression.
 * 
 * @author zeil
 *
 */
public
class NumericLiteral extends Expression
{
    ⋮
        /**
         * Create a default numeric literal. Equivalent to NumericLiteral("0");
         */
        public NumericLiteral ()
        {
	    ⋮
        }

        public NumericLiteral (String lit)
        {
	    ⋮
        }

        /**
         *  How many operands does this expression node have?
         *  
         * @return # of operands required by this operator
         */
        public int arity()                    {return 0;}

        /**
         *  Get the k_th operand
         * @param k operand number
         * @return k_th operand if 0 < k < arity()
         * @throws IndexOutOfBoundsException if k is outside of those bounds
         */
        public Expression operand(int k)      
        {
	    ⋮
        }


        /**
         *  Evaluate this expression, using the provided spreadsheet to resolve 
         *  any cell references.
         *  
         * @param usingSheet spreadsheet form which to obtain values of 
         * cells referenced by this expression
         * 
         * @return value of the expression or null if the cell is empty
         */
        public Value evaluate(Spreadsheet s)  
        {
	    ⋮
        }



        /**
         * Copy this expression (deep copy), altering any cell references
         * by the indicated offsets except where the row or column is "fixed"
         * by a preceding $. E.g., if e is  2*D4+C$2/$A$1, then
         * e.copy(1,2) is 2*E6+D$2/$A$1, e.copy(-1,4) is 2*C8+B$2/$A$1
         * 
         * @param colOffset number of columns to offset this copy
         * @param rowOffset number of rows to offset this copy
         * @return a copy of this expression, suitable for placing into 
         *           a cell (ColOffSet,rowOffset) away from its current position.
         *             
         */
        @Override
        public NumericLiteral clone (int colOffset, int rowOffset)
        {
	    ⋮
        }


        // The following control how the expression gets printed by 
        // the default implementation of put(ostream&)

        /**
         * Attempt to convert the given string into an expression.
         * @param in
         * @return
         */
        public boolean isInline()      {return true;}


        /**
         * If true, print in inline form.
         * If false, print as functionName(comma-separated-list).
         * 
         * @return indication of whether to print in inline form.
         * 
         */
        public int precedence()        {return 1000;}

        /**
         * Returns the name of the operator for printing purposes.
         * For constants/literals, this is the string version of the constant value.
         * 
         * @return the name of the operator for printing purposes.
         */
        public String getOperator()    {return literal;}

        @Override
        public boolean equals(Object obj) {
	    ⋮
        }

        @Override
        public int hashCode() {
	    ⋮
        }
        



}

I’m not showing the actual implementaiton (function bodies) here because

2.3.3 Writing the Unit tests

NumericLiteral desperately needs a good set of unit test cases because we need to be sure that we have covered all of the various input format possibilities:

TestNumericLiteral.java
/**
 * 
 */
package edu.odu.cs.espreadsheet.expressions;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.junit.Test;

import edu.odu.cs.espreadsheet.ExpressionParseError;
import edu.odu.cs.espreadsheet.Spreadsheet;

/**
 * @author zeil
 *
 */
public class TestNumericLiteral {
        
        
        public final NumericLiteral nl0 = new NumericLiteral("2.345");
        public final Spreadsheet ss = new Spreadsheet(); 

        /**
         * Test method for {@link edu.odu.cs.espreadsheet.expressions.NumericLiteral#NumericLiteral()}.
         */
        @Test
        public final void testNumericLiteral() {
                NumericLiteral nl = new NumericLiteral();  ➀
                assertEquals(0,  nl.arity());              ➁
                try {
                        nl.operand(0);
                        fail("Expected IndexOutOfBoundsException");
                } catch (IndexOutOfBoundsException ex) { 
                        // OK
                }
                assertEquals (0.0, nl.evaluate(ss).toDouble(), 1.0E-10);
                assertEquals ("0", nl.toString());
                assertEquals (true, nl.isInline());
                assertTrue (nl.precedence() > 100);
                assertEquals ("0", nl.getOperator());
                assertFalse (nl0.equals(nl));
        }

        /**
         * Test method for {@link edu.odu.cs.espreadsheet.expressions.NumericLiteral#NumericLiteral(java.lang.String)}.
         */
        @Test
        public final void testNumericLiteralString() {
                NumericLiteral nl = new NumericLiteral("2.3450");
                assertEquals(0,  nl.arity());
                try {
                        nl.operand(0);
                        fail("Expected IndexOutOfBoundsException");
                } catch (IndexOutOfBoundsException ex) { 
                        // OK
                }
                assertEquals (2.345, nl.evaluate(ss).toDouble(), 1.0E-10);
                assertEquals ("2.3450", nl.toString());
                assertEquals (true, nl.isInline());
                assertTrue (nl.precedence() > 100);
                assertEquals ("2.3450", nl.getOperator());
                assertFalse (nl0.equals(nl));
        }

        /**
         * Test method for {@link edu.odu.cs.espreadsheet.expressions.NumericLiteral#clone(int, int)}.
         */
        @Test
        public final void testCloneIntInt() {
                NumericLiteral nl = nl0.clone(1,1);
                assertEquals(0,  nl.arity());
                try {
                        nl.operand(0);
                        fail("Expected IndexOutOfBoundsException");
                } catch (IndexOutOfBoundsException ex) { 
                        // OK
                }
                assertEquals (nl0.evaluate(ss), nl.evaluate(ss));
                assertEquals (nl0.toString(), nl.toString());
                assertEquals (nl0.isInline(), nl.isInline());
                assertEquals (nl0.precedence(), nl.precedence());
                assertEquals (nl0.getOperator(), nl.getOperator());
                assertEquals (nl0, nl);
        }

        /**
         * Test method for {@link edu.odu.cs.espreadsheet.expressions.Expression#clone()}.
         */
        @Test
        public final void testClone() {
                NumericLiteral nl = (NumericLiteral)nl0.clone();
                assertEquals(0,  nl.arity());
                try {
                        nl.operand(0);
                        fail("Expected IndexOutOfBoundsException");
                } catch (IndexOutOfBoundsException ex) { 
                        // OK
                }
                assertEquals (nl0.evaluate(ss), nl.evaluate(ss));
                assertEquals (nl0.toString(), nl.toString());
                assertEquals (nl0.isInline(), nl.isInline());
                assertEquals (nl0.precedence(), nl.precedence());
                assertEquals (nl0.getOperator(), nl.getOperator());
                assertEquals (nl0, nl);
        }

        /**
         * Test method for {@link edu.odu.cs.espreadsheet.expressions.Expression#parse(java.lang.String)}.
         * @throws ExpressionParseError 
         */
        @Test
        public final void testNumericParse() throws ExpressionParseError {
	    String input = "12.34";                          ➂
                Expression e = Expression.parse(input);
                assertTrue (e instanceof NumericLiteral); 
                assertEquals (new NumericLiteral(input), e);
                
                input = "1.2E25";
                e = Expression.parse(input);
                assertTrue (e instanceof NumericLiteral); 
                NumericLiteral nl = (NumericLiteral)e;
                assertEquals (1.2E25, new Double(nl.getOperator()).doubleValue(), 1.0E20);
                
                input = "1.2E-5";
                e = Expression.parse(input);
                assertTrue (e instanceof NumericLiteral); 
                nl = (NumericLiteral)e;
                assertEquals (1.2E-5, new Double(nl.getOperator()).doubleValue(), 1.0E-10);
                
        }

}

This pattern is repeated for each of the mutators.

2.4 Is this Overkill?

Many, perhaps most, ADTs provide a small number of operations that manipulate the data in a non-trivial fasion, and a lage number of get/set attribute functions that, conceptually at least, simply store and retrieve a private data member.

For example, suppose we wanted to test the following ADT:

Address.java
package mailinglist;
/**
 * A contact is a name and address.
 * <p>
 * For the purpose of this example, I have simplified matters 
 * a bit by making both of these components simple strings. 
 * In practice, we would expect Address, at least, to be a
 * more structured type.
 * 
 * @author zeil
 *
 */
public class Address implements Cloneable {

    private String name;
    private String streetAddress;
    private String city;
    private String state;
    private String zipCode;

    /**
     * Create an address with all empty fields.
     *
     */
    public Address ()
    {
        name = "";
        streetAddress = "";
        city = "";
        state = "";
        zipCode = "";
    }

    /**
     * Create an address.
     */
    public Address (String nm, String streetAddr, String city, 
            String state, String zip)
    {
        name = nm;
        streetAddress = streetAddr;
        this.city = city;
        this.state = state;
        zipCode = zip;
    }



    /**
     * @return the theName
     */
    public String getName() {
        return name;
    }

    /**
     * @param theName the theName to set
     */
    public void setName(String theName) {
        this.name = theName;
    }

    /**
     * @return the streetAddress
     */
    public String getStreetAddress() {
        return streetAddress;
    }

    /**
     * @param streetAddress the streetAddress to set
     */
    public void setStreetAddress(String streetAddress) {
        this.streetAddress = streetAddress;
    }

    /**
     * @return the city
     */
    public String getCity() {
        return city;
    }

    /**
     * @param city the city to set
     */
    public void setCity(String city) {
        this.city = city;
    }

    /**
     * @return the state
     */
    public String getState() {
        return state;
    }

    /**
     * @param state the state to set
     */
    public void setState(String state) {
        this.state = state;
    }

    /**
     * @return the zipCode
     */
    public String getZipCode() {
        return zipCode;
    }

    /**
     * @param zipCode the zipCode to set
     */
    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }

    /**
     * True if the names and addresses are equal 
     */
    public boolean equals (Object right)
    {
        Address r = (Address)right;
        return name.equals(r.name)
                && streetAddress.equals(r.streetAddress)
                && city.equals(r.city)
                && state.equals(r.state)
                && zipCode.equals(r.zipCode);
    }

    public int hashCode ()
    {
        return name.hashCode() + 3 * streetAddress.hashCode()
        + 5 * city.hashCode()
        + 7 * state.hashCode()
        + 11 * zipCode.hashCode();
    }

    public String toString()
    {
        return name + ": " + streetAddress + ": " 
                + city + ", " + state + " " + zipCode;
    }

    public Object clone()
    {
        return new Address(name, streetAddress, city,
                state, zipCode);
    }

}

You can see that, in this case, the bulk of the operations are simple gets and sets. There are a few operations that work, in some sense, on the whole ADT, mainly for the purpose of output and comparisons.

If we were to test this, we would identify the mutators (the two constructors, the five set… functions, and the clone function) and accessors (the five get… functions and the toString, equals, and hashCode functions). Then we would create a test function for each mutator. For example, for the setCity function, we might write:


public class TestAddress {

    final private String name0 = "John Doe";
    final private String street0 = "221B Baker St.";
    final private String city0 = "Podunk";
    final private String state0 = "IL";
    final private String zip0 = "01010";

    ⋮

@Test
public final void testSetCity() {

	String city1 = "Norfolk";   ➀
	Address addr0 = new Address(name0, street0, city0, state0, zip0);
	Address addr1 = new Address(name0, street0, city0, state0, zip0);

	addr1.setCity(city1);   ➁

	assertEquals (name0, addr1.getName());    ➂
	assertEquals (street0, addr1.getStreetAddress());
	assertEquals (city1, addr1.getCity());
	assertEquals (state0, addr1.getState());
	assertEquals (zip0, addr1.getZipCode());

	assertFalse (addr1.equals(addr0));
	assertTrue (addr1.toString().contains(city1));
}

This follows a pattern that should be increasingly familiar:

You can see that four out of the first five assertions actually assert that this value was unaffected by the mutator. Some students look at this and wonder why we bother. Isn’t that a lot of wasted code? It doesn’t seem to really be relevant to the mutator function (setCity) that we are testing in this function. And when you consider that there will be similar “waste” in each of the other functions for testing the other mutator functions, this can seem excessive.

But there are a number of reasons why these “does not change” assertions are worth performing:

In general, it’s always a bit dangerous to argue that tests are unnecessary based on our intuition about how the code will behave when it runs correctly. Our choice of tests really have to be more informed by the possibilities of how the code might misbehave when it has bugs.

Furthermore, these “extra” assertions are not really all that oppressive. Most of them, written once, can be copied and pasted into the next test function. For example, the testSetState function could be:

@Test
public final void testSetState() {

	String state1 = "VA";
	Address addr0 = new Address(name0, street0, city0, state0, zip0);
	Address addr1 = new Address(name0, street0, city0, state0, zip0);

	addr1.setState(state1);

	assertEquals (name0, addr1.getName());
	assertEquals (street0, addr1.getStreetAddress());
	assertEquals (city0, addr1.getCity());
	assertEquals (state1, addr1.getState());
	assertEquals (zip0, addr1.getZipCode());

	assertFalse (addr1.equals(addr0));
	assertTrue (addr1.toString().contains(state1));
}

The highlighted portions are the only changes from the earlier testSetCity function.


A final comment on the “Is this excessive?” question:

3 Be Independent:


Is there a virtue to independence?