Tech Talk

Unit Testing with Complex Objects

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.

Editorial

By Reg. Charney

C++ Standards Meeting Report

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).

Trends

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.