Automatic
Testing
Site Map Feedback

Download:

Up Algorithms MFC Graphics Style Testing

"Automatic Testing" or "programmer Testing" (once called "Unit Testing") can be easy and is necessary, so do it!

There are many suites available to do testing [info here] but they all seem unnecessarily complex.

The target is simple: for each non-trivial unit (class/function) write tests that will inform you if they fail.

Like C++, Automatic Testing, or "making your code test itself" is a black art.
C++ has just over 60 key words. Most people can read a book on C++ in a weekend and remember enough to write a simple program...
But useful C++ coding requires so much more than knowledge of the keywords!
Similarly, the Tester class is tiny (15 lines), it will take you seconds to understand it, but making Automatic Testing work for you effectively will take time!

If your application is a DLL, a wonderful side-effect of the technique is that the tests run while you're compiling!
(The registration at the end of the compilation instantiates the Tester classes)

So here's what to do:
It's always wise to test the release version as well as the debug version in case the compiler's optimisations change the behaviour of your code in some unexpected way (it happens).
So use the Configuration Manager to create a new Configuration which is a copy of the Release Configuration and call it Beta.
Tell the Beta Configuration to generate Debugging info like the Debug Version in the C++ and Linker Sections.
Since the Beta Version has optimisations and debugging information, the debugging will not be good enough for single-stepping your program, but it will let you set breakpoints, and run the tests.
The final change to the project settings is to define NO_TESTS in the Release Version's C++ Preprocessor settings.
Include Tester.h in stdafx.h so that every file can see that the project has included it by testing for:

#ifdef Assert

Bear in mind that you need to test your Beta version as well as your Debug, so _DEBUG is not suitable!

After each object that you want to test create a Tester class which is derived from the CTester class.
Your derived Tester class will have a constructor which is full of Asserts to test the object you wish to test.

Instantiate the class in the header as a global and every time you run your application with testing enabled, the test will be run.
You can obviously instantiate at any time, depending on your needs.
Note that Assert will work in your Beta version as well as your Debug so that you can check for compiler bugs (differences in how your code works once the optimiser chews it up).
For example here's a strange class that gets defined and tested:

class CBadAdder {
  double d;
public:
  CBadAdder() {}
  CBadAdder(double D) : d(D) {}
            operator double()          const {return d;}
  CBadAdder operator+ (const double D) const {return CBadAdder(1+D+d);}
  bool      operator==(const double D) const {return d==D;}
  bool      operator!=(const double D) const {return d!=D;}
};

// Now the magic unit-testing class (in this case a struct because all members are public):
#ifdef Assert
static struct CBadAdderTester : Tester {
  CBadAdderTester() {
    CBadAdder x(1.1);
    Assert(3.3+x==4.4);
    Assert(x+3.3==5.4);
  }
} BadAdderTester;
#endif //def Assert

Since most classes will have many tests you should use lots of methods in the test struct to separate them. The constructor may end up looking as follows:

  CBadAdderTester() {
       ZeroTester();
   PositiveTester();
   NegativeTester();
  ToleranceTester();
  }

Testing Dialog Boxes

You can test dialog boxes by specifying in advance which exit button to press. So you could set up a dialog, tell the system to press cancel after you call DoModal, then carry on testing. Then you can repeat the operation but tell the system to press OK and then do further tests. This sort of test is only possible when the Dialog being tested has a Parent, so you need to be running a tester window/Dialog and the testing must be done after that window has been painted, not in the Tester's OnInitDialog(). To test Dialogs, derive your Tester Window/Dialog from Tester:

class CMyTesterDlg : public CDialog, public Tester {
...
};

At the end of the OnInitDialog() for the dialog you want to have tested, insert the following 3 lines:

#ifdef Assert // Allow this dialog to be tested:
  GetOwner()->PostMessage(WM_COMMAND, 0, (LPARAM)this);
#endif //def Assert
Add an OnCommand event handler to the Tester Dialog and give it the following implementation:
BOOL CMyTesterDlg::OnCommand(WPARAM wParam, LPARAM lParam) {return wParam ? CDialog::OnCommand(wParam, lParam) : Tester::OnCommand(lParam);}

Now, when you want to test a function which may open a dialog box, you can give the dialog an action to perform once it has opened:

  TryClicking(IDCANCEL); // You can't use this in OnInitDialog()
  CTestableDlg dlg;
  dlg.DoModal(); // This Dialog will instantly have IDCANCEL 'pressed'.
  Assert(dlg.GetDlgItemInt(IDC_SOMETHING)==123); //Check that The dialog default was correct

If the Load() function opened a Dialog Box with the above OnInitDlg code, the dialog would send a message to the Tester which would ask this class to process the message. This class performs the appropriate action on the Dialog Box, in this case, pressing the Cancel Button which allows the testing to proceed.

Compile-time Notes

Tester.h also provides functionality to make the compiler print notes in the output window which can help you remember where changes are needed in large projects.
You can jump them like error reports by pressing F4 or double-clicking the note's line in the Output Pane.
#pragma Note("This will become a string listed in the output window at compile time")
The pragmas available are Note, ToDo, Fix, and Ask (you could, of course, add others).

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


 (2040) Last modified: Fri, 15 May 2009 14:22:47 +0000