|
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 |
Files should be provided in the posting account soon. For now, just read the description, and get an idea of what's going on
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).
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.
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.
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.
Here are the steps you would take to develop classes in the "test, before you implement" style.
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.
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).
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.
| 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. |
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.
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
Let's think of a few basic tests we can perform, then write
them up.
Two ways to test this. Use isEmpty() and size(). It's usually easiest to start a test that checks the default constructor.
Again, another useful test. One way to test this is to use operator=. Another way to test this is to check the size, and pop off all elements, to see if they match. You can write tests for each kind (it makes some sense to do this because you will implement the copy constructor, and you want to make sure it works).
Of course, in this example, there's not a huge reason to test the copy constructor because it's not even defined (thus, the implicit version should work just fine).
However, it's still not a bad idea to test it. After all, you could have implemented it (assuming you had implemented the stack using dynamic memory allocation).
Create a stack then a copy, then pop off one value, and the two stacks should be different. See that it is.
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.
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.
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.
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.
The specifications for MovieQueue and MovieNode are shown below. Notice that you will need to implement copy constructors, destructors, and overloaded assignment operators.
| MovieNode |
Corrections
| Declaration | Description |
| Movie data | Holds information about one movie |
| MovieNode * next | Pointer to next node |
| 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 |
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).
| 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. |
| 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.
| 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). |
|
See the class syllabus for policies concerning email Last Modified: Sat Sep 14 16:07:25 EDT 2002 |
|
|
|
|
|