Exploring Solution Spaces © Copyright 2003-2006, by C. Keith Ray
   


About
Exploring Solution Spaces, Keith Ray's blog on Software development and other topics.

Send comments to:
keithray@mac.com

For Agile Training, eLearning, or Coaching contact:
Industrial Logic, Inc.
866-540-8336 (toll free)
510-540-8336 (Berkeley, California)

Links
xpminifaq
Résumé
“Adopting XP” Article 2002 (pdf)
“ Refactoring” Article 2006
AYE Conference
Lucien W. Dupont
Elisabeth Hendrickson
Johanna Rothman's Managing Product Development
Brian Marick's Exploration Through Example
Esther Derby's Insights You Can Use
Laurent Bossavit's Incipient(thoughts)
Dale Emery's Conversations with Dale
Martin Fowler's Bliki
Creating Passionate Users

Archives

  • 2003
  • 2004
  • 2005
  • 2006
  • 2007
  • 2008
  • Subscribe
    RSS Exploring Solution Spaces XML


           
    2004.Aug.08 Sun

    Part 1 - Test Driven Development from Model to Model-View-Controller

    This is an example of test-driven development. We're going to implement a simple calculator object test-first and hook it up to a user-interface. First thing we need to do is #include the test framework. I'm using my version of Michael Feather's CppUnit.

    #include "TestDrivenDesign.h"
    #include <string>
    #include <list>
    

    Now define our first test. I like to put these into a namespace.

    namespace SimpleCalcTests
    {
        DECLARE_TEST(CalcDisplayStartsEmpty);
        DECLARE_TEST(CalcHistoryStartsEmpty);
    
        DEFINE_TEST(CalcDisplayStartsEmpty)
        {
            SimpleCalc aCalculator;
            assertStringsEqual( "", aCalculator.GetDisplay() );
        }
    
        DEFINE_TEST(CalcHistoryStartsEmpty)
        {
            SimpleCalc aCalculator;
            StringList astringList = aCalculator.GetHistory();
            assertLongsEqual( 0, astringList.size() );
        }
    
        TestSuite* Suite()
        {
            TestSuite* result = new TestSuite("SimpleCalcTests");
            result->addTest( new SimpleCalcTests::CalcDisplayStartsEmpty );
            result->addTest( new SimpleCalcTests::CalcHistoryStartsEmpty );
    
            return result;
        }
    }
    

    So I've decided that a calculator has a display representable by a string, and a history that is a string-list. In order to get this to compile, I better define the minimal classes required. Normally you only implement one test at a time, but these are two very simple tests. Slow C++ compile-link-run turn-around-time tends make you want to batch things up. Be careful to keep the batches small.

    typedef std::list StringList;
    
    class SimpleCalc
    {
    public:
        SimpleCalc()
        {
        }
    
        virtual ~SimpleCalc()
        {
        }
    
        virtual std::string GetDisplay() const
        {
            return "display";
        }
    
        virtual StringList GetHistory() const
        {
            StringList result;
            result.push_back( "history" );
            return result;
        }
    };
    

    Note that the initial implementations of GetDisplay() and GetHistory() are going to make the test fail. That is intentional; I want the tests to inially fail in order to test the tests . I also initially wrote a single test, but changed it to two tests, so that each test only tests one thing. It's too easy to get false negatives or false positives if a test is testing multiple things. If you can't do the "right thing" when things are simple, what's going to make you do the right thing when thigns are more difficult.

    And why did I make the destructor and other methods virtual? First, it's a habit I've developed - if there is inheritance and the destructor and other methods are not virtual, you can get certain bugs that can be hard to debug. Second, I'm doing Object Oriented Programming. Polymorphism is a big part of that, and C++ does run-time polymorphism by declaring things virtual. Third, NOT declaring a destructor or a method virtual is a form of optimization... and optimizing code before you have hard evidence that it needs it is "premature optimization". Just don't do it.

    I also need to declare a test-runner and main function...

    int main (int argc, char * const argv[]) 
    {
        TestRunner calcTests("CalcTests");
        calcTests.addTest( SimpleCalcTests::Suite() );
        calcTests.runAllTestsOnce();
    
        return 0;
    }
    

    Here's the output from this code so far:

    TestSuite start CalcTests
        TestSuite start SimpleCalcTests
            TestCase CalcDisplayStartsEmpty...  AssertionFailedError 
                'expected: '' but was: 'display'', line 77, 
                file 'simplecalc/main.cpp'
                AssertionFailedError 
                FAIL 0 seconds
            TestCase CalcHistoryStartsEmpty...  AssertionFailedError 
                'expected: 0 but was: 1', line 85, 
                file 'simplecalc/main.cpp'
                AssertionFailedError 
                FAIL 0 seconds
        TestSuite end SimpleCalcTests
    TestSuite end CalcTests
    
    !!!FAILURES!!!
    Test Results:
    Run:  2   Failures: 2   Errors: 0
    There were 2 failures: 
        1) line: 77 simplecalc/main.cpp "expected: '' but was: 'display'"
        2) line: 85 simplecalc/main.cpp "expected: 0 but was: 1"
    

    This output is a little verbose because at various times using this test-suite I have needed to see where crashes were occurring (thus output about which suite and test are beginning to run and finishing running) and where slow code/tests were. Yes, I do care about code-speed, but I measure before I optimize.

    Now that I have the tests failing, time to make them pass. In this case, by permitting the display string to get empty and the history list to be empty. I'll demonstrate the use of a test-suite. (TestRunner is a sub-class of TestSuite, by the way.)

    class SimpleCalc
    {
    public:
        [other code unchanged ...]
    
        virtual std::string GetDisplay() const
        {
            return "";
        }
    
        virtual StringList GetHistory() const
        {
            StringList result;
            return result;
        }
    };
    

    Here's the output of the passing tests. It's much more succinct.

        TestSuite start CalcTests
            TestSuite start SimpleCalcTests
                TestCase CalcDisplayStartsEmpty...  0 seconds
    			TestCase CalcHistoryStartsEmpty...  0 seconds
    		TestSuite end SimpleCalcTests
    	TestSuite end CalcTests
    	OK( 2 tests)
    

    Part two will get into the meatier part of the calculator.

    [/docs] permanent link