Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
So what does a unit test look like? Too often developers
just use some well-behaved input to produce some expected output, which they
inspect visually. Two dangers exist in this approach. First, programs don t
always receive only well-behaved input. We all know that we should test the
boundaries of program input, but it s hard to think about this when you re
trying to just get things working. If you write the test for a function first
before you start coding, you can wear your tester hat and ask yourself, What
could possibly make this break? Code a test that will prove the function you ll
write isn t broken, and then put on your developer hat and make it happen. You ll
write better code than if you hadn t written the test first.
The second danger is that inspecting output visually is
tedious and error prone. Most any such thing a human can do a computer can do,
but without human error. It s better to formulate tests as collections of Boolean expressions and have a test program report any failures.
For example, suppose you need to build a Date class
that has the following properties:
A date can be initialized with a string (YYYYMMDD), three
integers (Y, M, D), or nothing (giving today s date).
A date object can yield its year, month, and day or a string of
the form YYYYMMDD .
All relational comparisons are available, as well as computing
the duration between two dates (in years, months, and days).
Dates to be compared need to be able to span an arbitrary number
of centuries (for example, 1600 2200).
Your class can store three integers representing the year,
month, and day. (Just be sure the year is at least 16 bits in size to satisfy
the last bulleted item.) The interface for your Date class might look
like this:
//: C02:Date1.h
// A first pass at Date.h.
#ifndef DATE1_H
#define DATE1_H
#include <string>
class Date {
public:
// A struct to hold elapsed time:
struct Duration {
int years;
int months;
int days;
Duration(int y, int m, int d)
: years(y), months(m), days(d) {}
};
Date();
Date(int year, int month, int day);
Date(const std::string&);
int getYear() const;
int getMonth() const;
int getDay() const;
std::string toString() const;
friend bool operator<(const
Date&, const Date&);
friend bool operator>(const
Date&, const Date&);
friend bool operator<=(const
Date&, const Date&);
friend bool operator>=(const
Date&, const Date&);
friend bool operator==(const
Date&, const Date&);
friend bool operator!=(const
Date&, const Date&);
friend Duration duration(const Date&, const
Date&);
};
#endif // DATE1_H ///:~
Before you implement this class, you can solidify your grasp
of the requirements by writing the beginnings of a test program. You might come
up with something like the following:
//: C02:SimpleDateTest.cpp
//{L} Date
#include <iostream>
#include "Date.h" // From Appendix B
using namespace std;
// Test machinery
int nPass = 0, nFail = 0;
void test(bool t) { if(t) nPass++; else nFail++; }
int main() {
Date mybday(1951, 10, 1);
test(mybday.getYear() == 1951);
test(mybday.getMonth() == 10);
test(mybday.getDay() == 1);
cout << "Passed: " << nPass
<< ", Failed: "
<< nFail << endl;
}
/* Expected output:
Passed: 3, Failed: 0
*/ ///:~
In this trivial case, the function test( )
maintains the global variables nPass and nFail. The only visual
inspection you do is to read the final score. If a test failed, a more
sophisticated test( ) displays an appropriate message. The
framework described later in this chapter has such a test function, among other
things.
You can now implement enough of the Date class to get
these tests to pass, and then you can proceed iteratively until all the
requirements are met. By writing tests first, you are more likely to think of
corner cases that might break your upcoming implementation, and you re more
likely to write the code correctly the first time. Such an exercise might
produce the following version of a test for the Date class:
//: C02:SimpleDateTest2.cpp
//{L} Date
#include <iostream>
#include "Date.h"
using namespace std;
// Test machinery
int nPass = 0, nFail = 0;
void test(bool t) { if(t) ++nPass; else ++nFail; }
int main() {
Date mybday(1951, 10, 1);
Date today;
Date
myevebday("19510930");
// Test the operators
test(mybday < today);
test(mybday <= today);
test(mybday != today);
test(mybday == mybday);
test(mybday >= mybday);
test(mybday <= mybday);
test(myevebday < mybday);
test(mybday > myevebday);
test(mybday >= myevebday);
test(mybday != myevebday);
// Test the functions
test(mybday.getYear() == 1951);
test(mybday.getMonth() == 10);
test(mybday.getDay() == 1);
test(myevebday.getYear() == 1951);
test(myevebday.getMonth() == 9);
test(myevebday.getDay() == 30);
test(mybday.toString() == "19511001");
test(myevebday.toString() == "19510930");
// Test duration
Date d2(2003, 7, 4);
Date::Duration dur = duration(mybday, d2);
test(dur.years == 51);
test(dur.months == 9);
test(dur.days == 3);
// Report results:
cout << "Passed: " << nPass
<< ", Failed: "
<< nFail << endl;
} ///:~
This test can be more fully developed. For example, we haven t
tested that long durations are handled correctly. We ll stop here, but you get
the idea. The full implementation for the Date class is available in the
files Date.h and Date.cpp in the appendix.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |