Encapsulate New For Testability

One of many TestingPatterns:

When you implement a test harness as a set of StubObjects (or using MockObject, or something else: MockStubShunt) (which derive from the interfaces on which the tested object depends), then you need a factory object to encapsulate any "new" object creation within the tested class. This lets the tested class create your mock objects, thus allowing a test to get visibility and controllability of the operation of the class.

Example:

I have a simple class who's job is to accept an input stream (of objects), and group them according to a criteria. (The following example is a pseudo mix of C++/Java syntax -- assume everything may be virtual)

 class Item
 {
   int foo() const;
   int bar() const;
 }

class Group { virtual bool isInsertable(const Item*) const; // fn of foo, bar, and Group's state void insert(Item*); }

class Collator { void accept(Item*);

Receiver *m_receiver; }

class Receiver { void accept(Group*) }

Given this set of classes, how do we test a Collator.

I first create my testbench, which contains subclasses of all the above classes. These subclasses have very simple implementations of the functions. For example, where Group::isInsertable may be a complex function of Item::foo, and Item::bar, and the current state of the group, my fake Item contains an isInsertable() predicate that my fake group simply returns as its result. So I get very tight control of the group/item interaction.

My fake receiver will receive Groups, but I want it to receive fake groups (which might simply return a count, or linear list, of the inserted items; instead of whatever complex insertion behavior is in the real group). However, if my (real) Collator is using "new Group" whenever it needs a new group, then there is no way to force it to create fake groups. So, even if the 'real' functionality does not need a factory, my test cases do.

--DaveWhipp

In Java, if you define your package's external interface using interfaces instead of classes, this will happen automatically, because interfaces are not allowed to have constructors. It also allows you to have multiple implementations available (in addition to the mock object implementation). A good example is the java.sql package.

--BrianSlesinsky

I think I should finish the example:

The code in my Collator uses CollatedGroup, which implements the Group interface:

  void Collator::accept(Item* item)
  {
     if (_currentGroup->isInsertable(item)
     {
         _receiver->accept(_currentGroup);
         _currentGroup = new CollatedGroup;
     }
     _currentGroup->insert(item);
  }

If there is no story that requires the Group to be a variation point, then there is no reason to introduce a factory object for the group. However, when you want to test it, the only way to get control of the emitted Groups is to pass in a factory and avoid the explicit use of "new".

  void Collator::accept(Item* item)
  {
     if (_currentGroup->isInsertable(item)
     {
         _receiver->accept(_currentGroup);
         _currentGroup = _groupFactory->make();
     }
     _currentGroup->insert(item);
  }

One of the problems with doing this is working out how to pass in the Factory. The options seem to be: on the Collator ctor or as a setFactory method (scoped either by class or instance). This is an example of DesignForTestability: the design is slightly more complex (an extra class and variable) to facilitate testing. If you had stories that required different types of Group, then this would not be an issue because you'd probably need the factory anyway.

--DaveWhipp


CategoryCpp


EditText of this page (last edited August 19, 2004) or FindPage with title or text search