Inner class magic
In Listing 6, we used an anonymous inner subclass of AtmGui
to override the createTransaction
method. Because we only had to override one simple method, this was a
concise way to achieve our goal. If we were overriding multiple methods
or sharing the AtmGui
subclass between many tests, it could pay to create a full (non-anonymous) member class.
We also used an instance variable to store a reference to the mock
object. This is the simplest way to share data between the test method
and the specialization class. It is acceptable because our test
framework is not multithreaded or reentrant. (If it were, we would have
to protect ourselves with synchronized
blocks.)
Finally, we defined the mock object itself as a private inner class of the test class -- often a convenient approach because it is clearer to put the mock right next to the test code that uses it, and because inner classes have access to the instance variables of their surrounding class.
Better safe than sorry
Because we overrode the factory method to write this test, it turns out that we no longer have any test coverage of the original creation code (which is now inside the base class's factory method). It may be beneficial to add a test that covers this code explicitly. This can be as simple as invoking the base class's factory method and asserting that the returned object is of the correct type. For example:
|
Note that the inverse, assertTrue(t instanceof Transaction)
would not suffice, because a MockTransaction
is a Transaction
as well.
From factory method to abstract factory
At this point, you may be tempted to go one step further and replace the factory method with a full-fledged abstract factory object, as detailed in Design Patterns by Erich Gamma, et al. (see Resources). In fact, many would have started this approach with a factory object, rather than a factory method -- we did, but soon backed away.
Introducing a third object type (role) into the system has some potential disadvantages:
- It increases the complexity without a corresponding increase in functionality.
- It may force you to change the public interface to the target
object. If an abstract factory object must be passed in, then you must
add a new public constructor or mutator.
- Many languages have conventions attached to the concept of "factory" that may lead you astray. For instance, in the Java language, factories are often implemented as static methods; this is not appropriate in this situation.
Remember, the whole point of this exercise is to make the object easier to test. Often, designing for testability can push the object's API toward a cleaner, more modular state. But it can be taken too far. Test-driven design changes should not pollute the public interface of the original object.
In the ATM example, as far as the production code is concerned, the AtmGui
object only ever makes the one type of Transaction
object (the real kind). The test code would like it to produce a
different type (a mock). But forcing the public API to accommodate
factory objects or abstract factories, just because the test code wants
it to, is the wrong design. If production code has no need to
instantiate many types of this collaborator, then adding that ability
will make the resulting design needlessly hard to understand.
View Unit testing with mock objects Discussion
Page: 1 2 3 4 5 6 Next Page: Resources