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


           
    2007.Jul.29 Sun

    Three A's and an E

    William Wake described the essence of an automated test as Arrange, Act, Assert. I've added "Erase", to account for the clean-up that some tests have to do. You might ask why "Erase" added to "Arrange, Act, Assert" and not some word starting with "A". I think starting with "eh?" is close enough. :-)


    "The thing about elves is they've got no ... begins with m," Granny snapped her fingers irritably."

    "Manners?"

    "Hah! Right, but no."

    "Muscle? Mucus? Mystery?"

    "No. No. No. Means like ... seein' the other person's point of view."

    Verence tried to see the world from a Granny Weatherwax perspective, and suspicion dawned.

    "Empathy?"

    "Right. None at all. Even a hunter, a good hunter, can feel for the quarry. That's what makes 'em a good hunter. Elves aren't like that. They're cruel for fun [...]"

    —Terry Pratchett, Lords and Ladies


    In C++, with certain test frameworks, a test might be specified in a manner something like the following in C++.

    TEST(TestBlurImageFilter)
    {
        // Arrange
        string outfileName = NewTempFileName("TestImageFilter");
        Image* sourceImage = new Image("lena.png");
    
        // Act
        ImageFilter* filter = new BlurImageFilter();
        filter->ProcessToFile(sourceImage, outfileName);
    
        // Assert
        AssertImagesEqual("expected_lena_blurred.png", outfileName);
    
        // Erase
        DeleteTempFile(outfileName);
        delete filter;
        delete sourceImage;
    }
    

    Note: generally you don't want to deal with files in unit tests; working in-memory would be much faster. Also, if this is one of those frameworks that throws an exception, or otherwise aborts the test if an assertion fails, then the "Erase" portion of the test won't get executed if AssertImagesEqual failed. Let's assume that's not a problem for the moment.

    Let's imagine that you then write a another test like so:

    TEST(TestUnblurImageFilter)
    {
        // Arrange
        string outfileName = NewTempFileName("TestImageFilter");
        Image* sourceImage = new Image("lena.png");
    
        // Act
        ImageFilter* filter = new UnblurImageFilter();
        filter->ProcessToFile(sourceImage, outfileName);
    
        // Assert
        AssertImagesEqual("expected_lena_unblurred.png", outfileName);
    
        // Erase
        DeleteTempFile(outfileName);
        delete filter;
        delete sourceImage;
    }
    

    Now you've got duplicated "Arrange" and "Erase" sections. And duplicated logic in tests can be just as bad it would be in production code. Fortunately, most test frameworks already have support for extracting "Arrange" and "Erase" to methods in a "test fixture". The above code could be refactored to something like the following:

    class ImageFilterTests : public TestFixture
    {
    public:
        ImageFilterTests()
            : sourceImage(NULL), filter(NULL)
        {
        }
    
        string outfileName;
        Image* sourceImage;
        ImageFilter* filter;
    
        virtual void SetUp()
        {
            // Arrange
            outfileName = NewTempFileName("TestImageFilter");
            sourceImage = new Image("lena.png");
        }
        virtual void TearDown()
        {
            // Erase
            DeleteTempFile(outfileName);
            delete filter;
            delete sourceImage;
        }
    };
    
    TEST_USING_FIXTURE(ImageFilterTests, TestBlurImageFilter)
    {
        // Act
        filter = new BlurImageFilter();
        filter->ProcessToFile(sourceImage, outfileName);
    
        // Assert
        AssertImagesEqual("expected_lena_blurred.png", outfileName);
    }
    
    TEST_USING_FIXTURE(ImageFilterTests, TestUnblurImageFilter)
    {
        // Act
        filter = new UnblurImageFilter();
        filter->ProcessToFile(sourceImage, outfileName);
    
        // Assert
        AssertImagesEqual("expected_lena_unblurred.png", outfileName);
    }
    

    Not only has this eliminated the duplicated logic, most unit test frameworks will also guarantee running the TearDown method even if the test fails, so you don't have to write your own try/catch blocks or other contortions for exception-safe "erase".

    You'll see that I also added a constructor to insure that the pointer variables have valid NULL values so we don't delete garbage pointers if the Image or Filter objects were not allocated successfully. (You should consider using boost::shared_ptr and/or boost::scoped_ptr if you're dealing with object pointers in C++ code and tests, by the way.)

    In those C++ test frameworks where the test-fixture creation and deletion is done just before and after executing the test, the SetUp and TearDown methods can (almost always) be replaced with a constructor and destructor instead. Using that and boost::scoped_ptr to insure exception-safe object deletion would allow us to write the following code:

    class ImageFilterTests : public TestFixture
    {
    public:
        ImageFilterTests()
            : outfileName(NewTempFileName("TestImageFilter")),
              sourceImage(new Image("lena.png"))
        {
            // Arrange
        }
    
        virtual ~ImageFilterTests()
        {
            // Erase
            DeleteTempFile(outfileName);
        }
    
        string outfileName;
        boost::scoped_ptr<Image> sourceImage;
        boost::scoped_ptr<ImageFilter> filter;
    };
    
    TEST_USING_FIXTURE(ImageFilterTests, TestBlurImageFilter)
    {
        // Act
        filter.reset(new BlurImageFilter());
        filter->ProcessToFile(sourceImage.get(), outfileName);
    
        // Assert
        AssertImagesEqual("expected_lena_blurred.png", outfileName);
    }
    
    TEST_USING_FIXTURE(ImageFilterTests, TestUnblurImageFilter)
    {
        // Act
        filter.reset(new UnblurImageFilter());
        filter->ProcessToFile(sourceImage.get(), outfileName);
    
        // Assert
        AssertImagesEqual("expected_lena_unblurred.png", outfileName);
    }
    

    [/docs] permanent link