C M S C     2 1 4
C o m p u t e r   S c i e n c e   I I
F a l l   2 0 0 2


Unit Testing a Queue Class
An Extreme Programming Idea

Files should be provided in the posting account soon. For now, just read the description, and get an idea of what's going on

Corrections

Purpose

Recently, there have been a large number of books devoted to the topic of extreme programming. While this sounds like some kind of programming for skateboarders, bikers, and inline skates doing half-pipes for some ESPN (or should that be EXPN?) competition, extreme programming is not that extreme. The ideas are somewhat controversial in the programming community.

The basic ideas of extreme programming (XP, for short) are:

We're going to consider the last idea: frequent testing. (By the way, Windows XP, as far as I know, has nothing to do with XP as extreme programming. Microsoft and the XP folk just happened to pick same two letters).

Unit Testing

Everyone says you should test your code (at least, in programming classes). This is quicker than proving (mathematically) your code works "correctly".

In particular, we want you to test the classes you write. This kind of testing is called unit testing. For programming languages like C++, unit refers to a class.

One way to do this kind of testing is to create a "unit testing" class specifically to test a class. Java now has classes built for doing unit testing. Those classes are part of a framework called JUnit. If you're interested in reading about this (not necessary, but a good idea), try http://junit.sourceforge.net/.

Some people have developed unit testing classes for C++, although it's not part of the C++ standard. However, for this class, we'll use a simple version of unit testing classes.

White vs. Black Box Testing

There are (at least) two kinds of testing one can perform on a class: black box testing and white box testing.

In black box testing, you test the class by calling public methods on objects of the class. In particular, you declare an instance of the object you wish to test, call public methods on it, and determine if the object is behaving as expected.

Since you are manipulating the object through public methods only, you do not have any special privileges to access the object's private data members, therefore you can not perform special tests based on specific implementation of the class.

For example, suppose you have a Stack class with operations such as push() and pop(). Since you can't see the underlying representation, you can't perform tests that determine if, say, a linked list, were correctly linked. Such tests can only be performed inside the class, where you have access to the private data members.

In white box testing, you test the implementation of the class. In addition to accessing public methods, you have access to private data members. For example, if you implemented a class with a binary search tree, you could determine if the tree preserves certain properties. Such testing could not occur without access to private data members.

You can also do coverage tests. Coverage tests attempt to cause every line of code in the class to run. To understand how this works, imagine you're looking at the code for some class. Pick any line of code, and some coverage test should cause that piece of code to run.

For if-else statements, you write one test where the "if" part is true (and its body executed), and another to check when the "else" is true (and its body executed). For loops, you write tests which cause a loop not to run at all (i.e., where the loop condition is false), or to cause various conditions in the loop to become false.

In this kind of testing, you assume that any code that doesn't run is either useless (because there may be no condition that ever causes the code to run) or more likely, untested, and therefore a test needs to be create to check that region of code. There are tools that help you check how much code coverage a test suite has. We won't be using those tools in this class, but it's useful to know they exist (profilers are a kind of coverage tool, though they are mostly used to determine where the time is being spent as the code runs).

No method of testing is perfect, but the more tests a piece of code passes, the more confidence you have in the code.

You will do a little of both kinds of testing, though primarily concentrating on black box testing. The main advantage of black box testing is that you can change the implementation, and still use the same tests.

By contrast, the advantage of white box testing is that you can test the properties of the implementation.

Other Forms of Testing

There are other ways to test code. For example, you can try to test the user interface, or how the entire application runs as a whole. For this course, you'll primary test your application by writing your own secondary inputs. Of course, such tests are not automated, and there are advantages to automation. In particular, you don't have to evaluate the output of the result. The program does it for you.

Writing Tests Before Coding

When do you test your code?

Most programmers test their code AFTER they write it. They don't see a particular reason to do it before. In fact, it seems to make no sense to test beforehand. After all, what is there to test?

But it does make sense to write tests prior to coding a class.

The reason? When you write a class, you are often providing an abstraction to the user (programmer). For example, you might be developing a Set class. The user who wants to use the class would like to think s/he is using a set, and manipulate the object as if it were a set.

Thus, you have to imagine how a set would behave. You can do this even if you have no idea how you plan to implement a Set class. When you write a class, you want provide such operations as set union, set difference, and membership in a set. Since you have some idea in your head how these methods ought to behave, you should be able to write tests using these methods, even if you aren't sure how to implement the method.

By thinking about the tests prior to coding, you force yourself to think of its abstract behavior. You force yourself to think about how the object will behave. This makes the task of implementing a class clear in your head. Many mistakes are made when you're not sure how you want your class to behave. And, even better, writing test code means that when you do finally have the code implemented, you have real code to test the class.

The goal...

So, the goal of this task is to make you write the testing code early, prior to implementing the class. We hope you are convinced that this is a good idea, and will use this technique to develop classes from now on. (Alas, it's difficult to unlearn bad habits---most of you probably still wait until the last minute to comment code, and would use public data members if we didn't restrict it).

Here are the steps you would take to develop classes in the "test, before you implement" style.

  1. Write the header file, and document each method.

    This gives you some idea of what you want the class to do. Initially, you should only specify the public methods. The private data members can come later, as you decide how the class should be implemented. The first step is to determine what methods the class supports, and how it behaves abstractly.

  2. Write a unit test class, to test the class

    At this point, you write out the tests. By writing the tests early, you do several things. First, you think about the behavior of the class. You can test whether the copy constructor works, the assignment operator, the equality operator, and so forth. You will use "asserts" to help out. Assertions will be explained momentarily.

    Second, once you have implemented the code, you actually have code that you can run to see if it works. (Of course, you may have to comment some of the tests out, until you implement the methods---therefore, it makes sense to write the test code in roughly the order you intend to implement the methods).

  3. Implement the code and test.

    Try to implement only a few methods at a time, before testing. In particular, stub the functions that aren't implemented.

    A function stub is something you put in the implementation of a method to make it compile. It's a place holder.

    For example,

    
    bool Foo::checkIfStatusAvailable() const {
       // STUB!
       return true ;
    }
    
    
    In this case, we have some method called checkIfStatusAvailable(). This method could be complicated. However, for the purposes of compiling, it just needs to return a boolean value, so we return true (it could also return false---it doesn't really matter). Then, add a comment like "STUB!", to remind you it's a stub, and needs to be filled in later on.

Steps 2 and 3 (writing the tests, then implementing the code) don't have to be clearly separated. That is, you don't necessarily have to write all of the tests prior to implementing code.

You may wish to alternate writing the test, and implementing the code. Also, your first tests are likely to be very simple, because you won't have many methods written.

Assert Class

You will be provided an Assert.h class. This class currently contains only two operations. They are listed below.

Static Public Methods

static void assertTrue( const string & str, bool expr ) ;
str is an "error" string. This is what gets printed if the expr is false. If expr is true, the method returns true, and prints nothing. If it's false, it will print a message, and throw an AssertException. (Don't worry about what an exception is, it's not that important when you use this method. However, feel free to read about exceptions in your text, if you are interested).
static void assertFalse( const string & str, bool expr ) ;
str is an "error" string. This is what gets printed if the expr is true. If expr is false, the method prints nothing. If it's true, it will print a message, and throw an AssertException.

Using assertions

Programs often exhibit properties during the course of execution, properties you wish to check are true or false at certain points in the program. You can use "assertions" to check those properties.

For example, you may assert that a pointer is NULL, or that the value of a variable is never negative, or that a list is sorted.

If there is a point in the program where you think a pointer should be NULL, then write an assertTrue() statement.

Here's how to write an assertion (using the Assert class).


   Assert::assertTrue( "[func] pointer not NULL", ptr == NULL ) ;

assertTrue() and (assertFalse()) take two arguments. The first argument is a string that is printed if the assertion is NOT true (or false, for assertFalse()). The second argument is the condition. In this case, the condition being checked is ptr == NULL.

If the condition is true, then nothing happens. The code continues to run as normal. If the condition is false, then an "exception" is thrown, which basically means an error has occurred, and your program will exit. (Switch "true" and "false" if you use assertFalse()).

Notice the name of the method (which we'll call "func") is printed so when the error message is printed, you know where to look in your code.

There are two things you can do at that point. First, you can find out what caused the condition to fail. There may be some error in your code that you need to fix. It may also be the case that your assertion condition is wrong, and what you assumed to be true, and you need to change the assertion. Either way, assertions help you find errors in your program, either by alerting you to something happening that shouldn't happen, or if it's supposed to happen, then by rethinking the condition.

Writing Test Code for Stack Class

To get an idea of how this works, we'll step through the development of a Stack class. You will then modify the code for a Queue class.

As you implement the code, you will also perform some white box testing, by using assert statements.

Here's a header for a MovieStack class, which has a stack of movies.


class MovieStack {
  std::vector stack ;
public:
  MovieStack() ;
  void push( const Movie & movie ) ;  // add movie to top of stack
  Movie pop() ;           // remove movie from top of stack
  int size() const ;      // return number of movies in stack
  bool isEmpty() const ;  // check if stack is empty

  // determine whether two stacks are equivalent
  bool operator==( const MovieStack & other ) const ; 
} ;

Let's think of a few basic tests we can perform, then write them up.

Of course, you should be able to come up with plenty of tests on your own (alas, this is a stack, so there's only so much you can do).

The next step is to create a "unit testing" class for MovieStack. To make the class, just add the word "Unit" to the end of the class name. This is only a convention, but it makes it easy for us to determine what are your unit testing classes. In this case, you create MovieStackUnit.

This is the header file for that class.

#ifndef MOVIE_STACK_UNIT_H
#define MOVIE_STACK_UNIT_H

#include <iostream>
#include <string>

#include "Movie.h"
#include "TestCase.h"

class MovieStackUnit : public TestCase {
public:
  MovieStackUnit( const std::string & methodName = std::string("NONE" ) ) ;
  void runTestCase() const ;
  void testEmpty() const ; // test to see if newly constructed stack is empty
  void testCopyConsPos() const ; // test copy cons, positive case
  void testCopyConsNeg() const ; // test copy cons, negative case
} ;

#endif
First, to test the class (this is black box testing), you should #include the header file for the class you are testing. In this case, since you're testing the Movie class, you should #include "Movie.h".

You should also #include "TestCase.h". This file will be provided to you in the posting account.

In order to write the unit testing class, we are using a feature of C++ called inheritance. At this point, you don't have to know that much about it. Just include the file. In a few weeks, we will discuss inheritance in depth.

The first thing you should notice is that you declare the class as class MovieStackUnit : public TestCase. This basically means that you are creating a class called MovieStackUnit which "inherits" from the TestCase. Methods that exist in TestCase also exist in MovieStackUnit. You don't have to copy the methods from TestCase to MovieStackUnit since inheritance does that for you.

How does inheritance affect you? It affects the methods you need to write for MovieStackUnit.

Because of the way TestCase is written, you need to have at least two methods in any unit testing class that inherits from this class. First, you should write a constructor exactly as shown above. It should take a const string reference as a parameter, which has a default value of "NONE". Just cut-and-paste, and rename the type of the constructor to the name of your unit testing class.

When someone creates a test, they will call the constructor, and pass in the name of the test method as a string. More details will be explained further below.

The second method you need to write (even more important) is runTestCase(). It should be a const method.

If the second method isn't written, then the code won't compile. TestCase (called the "base" class or "parent" class) forces MovieStackUnit (called the "derived" class or the "child" class) to implement runTestCase(). Again, you'll have to wait until a few weeks from now to understand why. For now, just write the prototype for runTestCase() as shown above.

Now, let's look at the implementation of these methods in MovieStackUnit. First, we look at the constructor.

MovieStackUnit::MovieStackUnit( const string & methodName ) 
  : TestCase( methodName )
{
}
What's going on here? The parameter passed in is the name of a method (as a string). This string is given to the constructor of the parent class (i.e., TestCase), which saves it to a data member in the parent class. That's what occurs in the initalizer list (i.e., TestCase( methodName )).

When you write your own unit testing class, you will simply copy this method, and replace MovieStackUnit with the name of your testing class.

Next, we look at runTestCase()

void MovieStackUnit::runTestCase() 
{
  string name = getMethodName() ;
  if ( name == "testEmpty" )  // testEmpty() is a test method
    testEmpty() ; 
  else if ( name == "testCopyConsPos" ) // testCopyConsPos() is a test method
    testCopyConsPos() ;
  else if ( name == "testCopyConsNeg" ) // testCopyConsNeg() is a test method
    testCopyConsNeg() ;
  else // unknown method
    cout << "Unknown method: " << name << endl ;
}
The first line fetches the name of the method. When you write a test class, it consist of: a constructor, the runTestCase() method, and test methods specific to the class you are testing.

getMethodName() returns the string that was passed in when the object was constructed. The string's name should be one of the test methods.

Thus, if you have methods called testEmpty(), testCopyPositive(), and testCopyNegative, you should use those names as strings (without parentheses).

Note: getMethodName() is implemented in TestCase, so that's why MovieStackUnit doesn't need to implement or declare it. It just "inherits" it from TestCase.

The constructor and runTestCase() will almost always look like the code you see above. The only difference between the code above, and the code you write are the names of the test methods. Your class is likely to have different names for test methods than those used in MovieStackUnit.

Notice each test method starts with the word "test". This is a convention that you should follow in your code.

Let's now take a closer look at testEmpty(). This is a test for MovieStack.

void MovieStackUnit::testEmpty() const 
{
  MovieStack mStack ;  // create empty stack
  Assert::assertTrue( "[testEmpty] isEmpty() fails", mStack.isEmpty() ) ;
  Assert::assertTrue( "[testEmpty] size = 0 fails", mStack.size() == 0 ) ;
}
In this test, we declare a MovieStack called mStack. We're going to check that it's empty in one of two ways. First, we check the isEmpty() call (which we expect to be true). Second, we check the size is 0 (which we expect to be true).

This is where you use the assertTrue static method of Assert. As described earlier, this method takes two parameters. The first parameter is a string that describes what error has occurred. The second parameter is a condition. Since this is a assertTrue method, then we expect the second condition to be true.

If this condition is true, then basically nothing happens. If it fails, then it prints the string, and an error occurs (this error turns out to be something called an exception. For the time being, you don't have to know what an exception is, though you can read your book if you're curious.

Notice it's useful to print the name of the test method in brackets, so if it does print (when an error occurs), you know which test method failed.

Here's another test.

void MovieStackUnit::testCopyPositive() const 
{
  MovieStack mStack ;  // create empty stack
  Movie m1 = "Jaws" ;
  Movie m2 = "Patton" ;
  Movie m3 = "Signs" ;

  // Create a stack with 3 elements
  mStack.push( m1 ) ;
  mStack.push( m2 ) ;
  mStack.push( m3 ) ;

  // call copy constructor
  MovieStack mStack2 = mStack ;  

  // test using operator==
  Assert::assertTrue( "[testCopyPositive] == fails", mStack == mStack2 ) ;
  // test size is same
  Assert::assertTrue( "[testCopyPositive] size fails", 
                       mStack.size() == mStack2.size() ) ;

  // pop each stack and check if elements are the same
  while ( ! mStack.isEmpty() )
    {
       Movie one = mStack.pop() ;
       Movie two = mStack.pop() ;
       Assert::assertTrue( "[testCopyPositive] movie check fails", 
                             one == two ) ;
    }
  // check if second stack is empty (it should be)
  Assert::assertTrue( "[testCopyPositive] mStack2 empty fails", 
                          mStack2.isEmpty() ) ;
}
This tests at least three methods: operator==, size(), and pop().

This test method does a lot. It could have been rewritten as several different tests. For example, you can easily split the above method into three different methods. One method could check operator==, the next could check the size. Finally, the last could check if the elements of each stack, one at a time.

In these tests, the goal is to determine what conditions you can check, and use asserts to verify if they are true (or false).

If you want, try writing code for testCopyNegative where you make a copy of one stack then pop off one element from it. Now the two stacks are different, and you can check operator==(), size() and so forth, to check that indeed, they are different.

Naming Conventions

As a convention, all methods that test should begin with the word test. In this example, you have testEmpty() and testCopyPositive. Both test methods begin with the word "test". This makes it easy to determine tests.

You can create helper methods that are private. More than likely, such methods will check if some condition is true or false, so it can be used by the one of the test methods.

Another convention is to name the unit test class by the name of the class you are testing followed by "Unit" to indicate it's unit testing that class.

Putting a TestSuite together

As you write a test class, you may wish to test all the test methods, or some subset of the test methods. You use a TestSuite class to run some or all the tests in a class. You do so by adding dynamically-allocated test case objects to a Test Suite object (which is basically a container class for holding test case objects).

Here's an example of how to use a TestSuite by writing a static method in MovieStackUnit called runTestSuite().

void MovieStackUnit::runTestSuite()
{
  TestSuite suite ;
  suite.addTest( new MovieStackUnit( "testEmpty" ) ) ;
  suite.addTest( new MovieStackUnit( "testCopyConsPos" ) ) ;

  suite.runTests() ;
}
Here's the idea. First, declare an object of type TestSuite. TestSuite basically stores zero or more test cases (it's a container class). To add a test case to the suite, dynamically allocated a test case (in this case, a MovieStackUnit object). Pass the name of the method (as a string) that you want to test, as an argument to the MovieStackUnit constructor. In this case, you see testEmpty passed as a string. That means, we want to test the testEmpty method in this test suite.

You also see testCopyConsPos added as a second test. suite now contains two test cases. It will call testEmpty and testCopyConsPos.

Once the tests have been added to the suite, call runTests() on the TestSuite object. When you call runTests() this will call the runTest() method of the two MovieStackUnit objects that it stores, in the order they were added to the test suite.

If the tests succeed, you see several periods. If any test fails, the program prints an error message and quits. It's then your job to go fix the error.

Summary

Let's go over the steps to create a test class.

  1. Create a test class with the name Unit after the class you wish to test. For example, if you want to test Foo create a test class called FooUnit.
  2. Create a header file that uses the : public TestCase (make sure to #include "TestCase.h"). This "inherits" from TestCase.
  3. Implement a constructor just like the one in MovieStackUnit
  4. Implement runTestCase() similar to the one in MovieStackUnit.
  5. Write a series of test case methods (you will probably write these first, then update runTestCase()) as needed.
  6. Write a static method for the class called runTestSuite(). Make sure to #include "TestSuite.h". Declare a TestSuite object in this method, and add tests as appropriate.
  7. Call the runTestSuite() (as a static method) on the suite.

Causing tests to fail...first

In general, you want to try to cause the test methods to fail first, before you get it to work. Why? If you happen to have a buggy implementation of the test method, it may always return true. You want to make sure that it's possible for the test method to be false, and cause an assertion to fail. By deliberately causing it to produce "false" for assertTrue (or "true" for assertFalse), you will gain confidence that your test code isn't buggy.

Don't put too much work into making the test fail. You want to make the test fail in the easiest way possible. For example, have isEmpty() just return false. All you want to do is see it fail once, then go implement, and see it pass the test.

Writing Test Code for MovieQueue Class

Now it's your turn to write a test class. You will do the following:

The specifications for MovieQueue and MovieNode are shown below. Notice that you will need to implement copy constructors, destructors, and overloaded assignment operators.

MovieNode Specifications

MovieNode

Corrections

Instance Variables

Declaration Description
Movie data Holds information about one movie
MovieNode * next Pointer to next node

Public Methods

MovieNode( const Movie & movieIn = Movie() ) ;
Default constructor. Sets next to NULL. Sets data to movieIn. Has an optional, default parameter. If constructor is called with no arguments, then a default Movie object is used for movieIn (Movie() calls the default constructor---note: it does so without calling new).
void setNext( MovieNode *nextIn ) ;
Sets instance variable, next, to the value of parameter, nextIn.
MovieNode * getNext() const ;
Return next.
Movie getData() const ;
Returns value of data.

MovieQueue Specifications

MovieQueue

A MovieQueue is a singly linked list of MovieNode nodes. It has a pointer to the first and last element of the list, to make it easier to insert and remove. This is NOT a sorted list. In particular, enqueue() always adds to the end of the list (where last is), and dequeue() always removes from the front of the list (where first is).

Instance Variables

Declaration Description
MovieNode * first, * last ; Points to first and last node of the queue.
int _size ; Number of nodes in the queue. (Invariant property: _size >= 0). The underscore is in front because otherwise its name would "clash" with the size() method's name.

Public Methods

MovieQueue() ;
Default constructor. Sets first and last to NULL. Set size to 0.
void enqueue( const Movie & movieIn ) ;
Adds a movie at the end of the queue (i.e. the element after last). This should be dynamically allocated.
bool dequeue( Movie & movieOut ) ;
If there is a movie in the queue, then it removes the movie from the front of the queue, and deallocates it. That movie should be copied to movieOut and true should be returned. However, if the queue is empty, return false and do not change the queue. (Note: this isn't a very elegant dequeue, but it will do for now).
void clear() ;
Empty the queue, deallocating nodes as needed.
bool isEmpty() const ;
Returns true if the queue is empty. Returns false, otherwise.
int size() const ;
Returns number of elements in the queue.

You should determine whether this class needs a copy constructor, assignment operator and destructor written. If so, add the methods above.

Then, implement the following private helper methods, and use them to implement the copy constructor, assignment operator and destructor.

Private Methods

void init() ;
Does what default constructor does (sets pointers to NULL, and size to 0). Should be called by the copy constructor before calling copy().
void copy( const MovieQueue & other ) ;
Copies other. Does NOT deallocate nodes. The precondition for this method is first and last are NULL and size is 0. Both the copy constructor and the overloaded assignment operator should call this.
void freeMem() ;
Deallocate the memory for all nodes. clear() should call this, as should the overloaded assignment operator, and the destructor. The copy constructor should NOT call this method (since there's no need for it to free memory).

MovieQueueUnit

You will be provided a partial MovieQueueUnit header file. You should implement the code, and test.


See the class syllabus for policies concerning email
Last Modified: Sat Sep 14 16:07:25 EDT 2002
left up down right home

Web Accessibility