Testing ADTs in C++

Steven J Zeil

Last modified: Feb 27, 2014

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());
}

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

Example: addContact - many sizes

testAdd1.cpp

Example: addContact - ordering

testAdd2.cpp

2. Be Thorough: Mutators and Accessors

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

Example: Unit Testing of the MailingList Class

mailinglist.h

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

Now we start going through the mutators.

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

The Check Function for Copying

shouldBeEqual.cpp

Testing the Assignment Operator

testAsst.cpp

Testing addContact

testAdd.cpp

And so on…

We continue in this manner until done.

fullTest.cpp

2.2 Testing for Pointer/Memory Faults

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

Tools for Testing Pointer/Memory Faults

3. Be Independent:

Is there a virtue to independence?

4. Be Pro-active: Write the Tests First

Ideally, we have made it easier to write self-checking unit tests than to write the actual code to be tested.

Debugging: How Can You Fix What You Can’t See?

The test-first philosophy is easiest to understand in a maintenance/debugging context.

Test-Writing as a Design Activity

Every few years, software designers rediscover the principle of writing tests before implementing code.

Agile and TDD (Test-Driven Development) are just the latest in this long chain.