|
By Reg. Charney
Unit Testing, An Introduction
I am defining a unit as a program or function that uses some
objects to do a task. These objects are non-trivial and require
preparation in terms of setting up their constructor arguments
before they can be instantiated. Further, we know that individual
classes have been tested and we know they work. What we are trying
to do is put them all together into meaningful whole. We are also
trying to avoid embedding the unit into a massive system. There are
several reasons for doing this, including the main system does not
work yet, or it takes up a lot of resources, etc. We also want the
testing to be as quick and simple as possible while giving
meaningful results.
Music Site Example
As part of our music site, our unit test logs in users, loads
their preferences and extracts matching music from a database. The
objects come from an authentication class, a user class, a
preference class, and a database class.
These classes have different characteristics. If the Database
can not be found or accessed, the unit fails catastrophically
(I.e., an assert fails.) Similarly
for the Auth class. If no
preferences can be found for a user, a default Pref
instance is acceptable. If a User fails,
only an indicator is needed because the user can try up to n
times, depending on parameter.
In pseudo-code, here is the unit:
string dbname, uid;
cout << "Enter db name:";
cin >> dbname;
Database db(dbname);
cout << "Enter
your id:";
cin >> uid;
Auth a(uid, 3);
Preference p(a);
Selection s(db, p);
|
In this example, Database db
needs a string for its name/path. Auth a needs
a string, Preference p needs a user,
and Selection s needs both a
database object and a preference set. There is a lot of setup needed
to test the viability of selection s.
Wouldn’t it be simpler to write something like this that would do
the same thing?
TDatabase db;
TAuth a;
TPreference p(a);
TSelection s(db, p);
|
The sample above shows what you can do with test classes. These
are classes that you design when you design the real classes. Notice
that the default test class constructors do what you want and
expect. This article discusses how you can do this.
Construction Techniques
There are two techniques used to construct an object: the normal
constructor method; and the static create method. The normal
constructor method usually appears in one of two ways:
class X { /* . . . */ };
X x(a1, a2);
X px = new X(a1, a2, a3);
|
The number of constructor arguments vary by class and application
use. With this method, failure to construct an object can only be
detected by: (i) throwing an exception; (ii) terminating the
process; or (iii) using a status variable. (Note: failure may be
independent of the allocation of object storage.)
The second technique uses a static create function for the class.
class X {
public:
static X* create(int a)
{ return a ? new X : 0; }
// . . .
private:
X() { cout << "In
X\n"; }
};
// . . .
A a;
X px = X::create(a);
|
An instance of the class X is
created only under some precondition, like when a
evaluates to non-zero . Otherwise, the create function returns null.
Basic Testing Technique
We use derivation to create test classes because they satisfy the
“is-a” criteria. Any derivative is an instance of a base class.
Thus, anything that you do to a base class instance, you can also do
to a derived class instance. For example,
class B { /* . . . */ };
class TB : public B { };
void foo(B& b);
void bar(B* pb);
B b, bp = &b;
TB tb, ptb = &tb;
foo(tb);
bar(ptb);
|
Both foo() and bar()
work as well with a TB as
with a B. Thus, we can use a TB
as easily as a B. This is the
basis of our testing technique. This article shows how we can easily
create derived test classes from normal class.
Simple Derivation
Going back to the music example, we can see that a Pref
class can return a default preference object if the user has
no preferences.
class Pref {
public:
typedef list<Music*> LM;
Pref() { /* . . . */ };
Pref(Auth& a)
{ lm = rdPref(a.path); }
private:
LM lm;
LM rdPref(const string &s);
};
|
This is the simplest case for creating a test class. In fact, a
test class is not really needed—we can just use the actual class.
However, a normal default preference may not be very interesting.
For example, it may be the same as no preference, which means that
it looks like it does not even exist. Thus, tests based on such a
preference would not be very useful. Better would be a dummy set of
preferences that would be non-empty and valid.
In this simple case, Step 1 would be to introduce a protected
constructor that does nothing.. It needs a signature that
distinguishes it from any other constructor. Step 2 would be to
define a derived class that loads a sample preference list.
class Pref {
public:
typedef list<Music*> LM;
Pref() { /* . . . */ };
Pref(Auth& a)
{ lm = rdPref(a.pathm); }
private:
LM lm;
LM rdPref(const string &s);
protected: // test only
Pref(int) { /* empty */ }
};
#include <new>
class TPref : public Pref
{
public:
TPref() : Pref(0)
{
Auth a("validAuth");
((Pref*)this)->~Pref();
new(this) Pref(a);
}
};
|
Here, TPref invokes the
constructor that does nothing, defines an authorized user using a
known valid test user, destroys the empty object part of itself, and
then creates a valid Pref instance
on top of itself. Now we can use TPref anywhere
that we could use Pref — even the
address has remainder the same. Please note the #include
<new>. It is needed for the placement new.
Static Create function Technique
Recall that the Auth class needs
to get a valid user. If an invalid user is entered, another n-1
tries are made. Thus, we want a way of signally that the
given string does not represent a valid User.
In such a case, the static create technique can come in handy.
class User {
public:
static User* create(string& s)
{
bool b = valid(s);
return b?new User(s):0;
}
void doSomething();
private:
User(string& s) { uid=s; }
bool valid(string s);
string uid;
};
cout << "Enter id:";
cin >> s;
User *pU = User::create(s);
if (pU) { // valid user
pU->doSomething();
}
|
Since the test class should support the same constructor
interface as the base class, we need a static class create
function. We are also going to use the placement new
trick that we used earlier, only its going to be a little
more complicated—but just a little. As before, we added a
do-nothing base class constructor different from any potential
default base constructor.
class User {
public:
static User* create(string& s)
{
bool b = valid(s);
return b?new User(s):0;
}
void doSomething();
private:
User(string& s) { uid=s; }
static bool valid(char*s);
string uid;
protected: // test only
User(int) { /* empty */ }
};
#include <new>
class TUser : public User {
public:
static TUser* create(string& s)
{
return (TUser*)
User::create(s);
}
};
cout << "Enter id:";
cin >> s;
TUser *pU=TUser::create(s);
if (pU) { // valid user
pU->doSomething();
}
|
This is simple and works fine. However, it does not support a
default constructor for the test class. First, notice that class User
already has a constructor, but its private.
To make the User constructors we
need available to the test class TUser,
the easiest solution is to declare TUser a
friend class. This makes them
available to any derived test class. Also, lets make all validation
functions static. (Note, we have already separated data validation
from the allocation of the memory in the create
function.)
class User {
friend class TUser;
public:
static User* create(string& s)
{
bool b = valid(s);
return b?new User(s):0;
}
void doSomething();
private:
string uid;
User(string& s) { uid=s; }
static bool valid(char*s);
User(int) { /* empty */ }
};
#include <new>
class TUser : public User {
public:
// Assume Ed always valid
TUser(char* s=0):User(0)
{
string ss;
ss = (s) ? s : "";
if (ss == "")
ss = "Ed";
((User*)this)->~User();
assert(valid(ss));
new(this) User(ss);
}
static
TUser* create(string& s)
{
return (TUser*)User::create(s);
}
};
TUser tu; // valid user Ed
tu.doSomething();
cout << "Enter
id:";
cin >> s;
TUser *pU=TUser::create(s);
if (pU) { // valid user
pU->doSomething();
}
|
Note the default TUser constructor
invokes the do-nothing User constructor
explicitly. The empty User that is
initially constructed as part of TUser can
also be destroyed. Since this part of the code is a constructor, if valid()
returns a false, the assertion fails. Else, we can construct
the new instance on top of ourselves. Again, remember to #include
<new> to use the placement new
operator. I can also safely cast up from a User
to a TUser since TUser
has no member data.
To summarize this technique, I defined a derived test class with
a default constructor and a public static create function. I also
made the test class a friend of the base class. As part of the test
class, a valid user value was inserted if no user id was supplied.
As a result of all this is that the test class can be used wherever
the original base class can be used. It can also be used with a
default constructor.
The music example was inspired by my neighbor Marietta and her
kids, Kelly and Robert, and a delightful Sunday Peninsula Youth
Orchestra concert. ( www.peninsulasym.org
) Thanks.
By Reg. Charney
Early this month I attended the C++ Standards Working Group
(WG21) in Copenhagen. It was held at the Danish Standards (DS)
locale on the outskirts of the city. In getting to the meeting place
by train, two things were noticeable: the trains ran on time and the
ticketing system they used was very neat. Ask if you are interested.
I attended the Library Working Group for the week, although I
normally attend the Core group. Most of the recent “exciting work”
has been done in the Library Working Group (lwg) and I wanted to get
a better feel for what has been happening there.
In summary three things have come out of this meeting:
1) We are within one meeting of issuing the next Technical Report
on the C++ Standard containing corrections and clarifications. As
voting members of WG21, we will have access to the Working Draft of
the TR that should be ready within 60 days.
2) Bjarne gave a presentation on his vision of the Future
Directions of C++. Bjarne may have posted his slides on his web site
by the time you read this.
3) Based on a number of Bjarne's comments and other issues that
have come up over the last couple of years, we are going to
resurrect the Extensions Working Group to consider things like his
eXtensions Language Interface (XLI).
By Reg. Charney
Job Openings
This is another month of declining job openings. There was a
15.8% decline in Silicon Valley IT job openings since last month.
Nationwide, the decline was 16.9%. The only positive trends were in
the Windows 2000 arena.


From the charts above, you can see that the growth in demand for
Windows 2000 skills are increasing at an accelerating rate. The most
dramatic falloff is in the Linux space. While demand is still strong
for Linux, the continued demand is falling off. It is my contention
that Linux is a “early adopter” phenomena. As such, people take
a chance on things like Linux while times are good and retreat from
chancy ventures when things are tough.
SourceForge
Statistics
Since last month the number of projects has increate 5.8% and the
number of users has increased by 9.2%. The most popular projects
downloaded this month are the CDex project, a CD-Ripper that
extracts digital audio data from an Audio CD. The GPL Windows
application supports many Audio encoders, like MPEG (MP2,MP3), VQF,
AAC encoders. The second most popular download is MiKTeX, an
up-to-date implementation of TeX & Friends for Windows (all
current variants). The third most popular project is MyNapster, a
Win32 client using Gnutella and IRC for chat. It is based on
Gnucleus and utilizes MFC (works with WINE).
Note that these projects are meant to run under Windows, not
Linux.
By project type, the projects on SourceForge are:
| Communications |
2564 projects |
| Database |
984 projects |
| Desktop Environ |
678 projects |
| Education |
465 projects |
| Games/Entertainment |
2339 projects |
| Internet |
3675 projects |
| Multimedia |
1998 projects |
| Office/Business |
683 projects |
| Other/Non-listed |
461 projects |
| Printing |
77 projects |
| Religion |
48 projects |
| Scientific/Engineering |
1220 projects |
| Security |
406 projects |
| Sociology |
27 projects |
| Software Development |
2566 projects |
| System |
3068 projects |
| Terminals |
106 projects |
| Text Editors |
386 projects |
As an aside, the story I heard about CDex is that they wrote to
the RIAA (Record Industry Association of America) asking what CDs
they could index and the RIAA replied by suing them. Ah, thank God
for lawyers.
Correction:
In the last month’s article, I changed the fourth example to
say: ^[^\s]*
Jesse corrected me. It should have been: ^[^ ]+. He said, “I
did not use the '\s' because that is not in all regex engines. It
should be '+' rather than '*' because we want a positive grab.
|