Mock Objects: Shortcomings and Use Cases
by Alex Ruiz
08/22/2007
Mocking concrete classes can be dangerous
Some mock object frameworks like EasyMock and JMock provide extensions that use cglib to enable this practice. cglib generates proxies at runtime for a given class by subclassing it and overriding the methods of interest. As a result, the class to mock and the methods involved in the expectations cannot be final, which limits our design decisions.
At the same time, a specific constructor signature (using reflection) may be necessary to instantiate a class-based mock, tightly coupling that constructor to one or more tests. Consequently, classes used as mocks are difficult to maintain, and tests are even more fragile, since it is not trivial to refactor code when usage of reflection is present, even with modern Java IDEs.
Regular mocks, the ones based on
interface
s, have their expectations set on public APIs, which can be considered more or less stable. In contrast, expectations on class-based mocks may depend on protected or package-protected methods, which represent implementation details of a class. Such implementation details can (and will) change at any time, changing the interaction between the code under test and mocks, increasing the chances of breaking existing tests.
Mocks may lead to
interface
overuse
A possible side effect of mock abuse is the unnecessary creation of Java
interface
s, for the sole purpose of mock creation (trying to avoid the problems related to class-based mocks.) Typical examples include creation of
interface
s that will have one and only one implementation, such as utility or helper classes. This practice is often justified by a misinterpretation of the principle "
Program to an interface, not an implementation." This principle refers to the
concept of interface, a
supertype used to exploit polymorphism, not the Java construct
interface
. It is possible to program to an interface, implemented using a Java
interface
or an abstract class.
Creating
interface
s to aid mock testing increases maintenance costs (because there is more code to maintain), which usually outweighs any benefit that mocks may bring.
Use Cases for Mocks
On the bright side, mock objects can be useful when used with discretion. The following are some possible good use cases for mocks.
Test-before-you-commit test suite
Having a fast-running test suite, to be executed by each developer before committing her local changes to the source control repository, can (obviously) speed up development. Mock objects can be used to build this test suite, as long as these tests can give us the confidence that our local changes are not going to accidentally introduce bugs to the code base. A classic example is testing
Servlet
s in isolation, using mocks for the
HttpServletRequest
,
HttpServletResponse
, and
HttpSession
objects, which is significantly faster and easier to set up than a real application server.
We can use mocks in our test suite as long as we keep in mind that these tests may be fragile, and at some point (for example, in the continuous integration build) we need to execute integration and functional tests as well.
Temporary testing of integration of components that have not been written yet
Mocks can be useful when integration of complex components is expected to occur in the future. For example, it would make sense for one team to use mock testing while they wait for a second team to finish their component. To minimize any integration problems, the second team can build and provide the mock to the first team. Once the second team has finished their work, integration between the components from both teams can take place, hoping that testing using mocks got them as close as possible to the expected system behavior.
At this point, mocks have served their purpose and, because of their potential shortcomings, should be removed, even for future testing.
Testing implementations of the Decorator design pattern
In the previous example,
how
EmployeeBO
stores employee information in the database was irrelevant, as long the data was stored correctly. In the case of
decorators, correct interaction between them and the decorated object is as important as the end result of such interaction. Consider the following simple (but unrealistic) example depicted in Figure 3.
Figure 3. Class diagram of a cache manager system
Figure 3 illustrates a cache management system, with the responsibility of storing frequently used objects in a cache as way to improve the performance of a system. The cache management system is composed of an interface
CacheManager
and two implementations,
DistributedCacheManager
and
EmbeddedCacheManager
, which are expected to be used in Web applications and rich-client applications, respectively.
Let's say we need to introduce a way to configure how the caching system handles
Exception
s. If the system is in production, errors in the caching system should not stop the execution of such system. In another words, any
Exception
thrown by the cache should be ignored (and perhaps logged). On the other hand, for integration testing we need to catch all
Exception
s thrown by the cache to diagnose and fix any implementation defect in the caching system.
This problem can be easily solved using the
Decorator Pattern. We can easily attach exception handling to any implementation of
CacheManager
dynamically, as a flexible alternative to inheritance.
public final class IgnoreExceptionsCacheManagerDecorator implements CacheManager {
private static final Object NULL = new Object();
private static Logger logger = Logger.getAnonymousLogger();
private final CacheManager decorated;
public IgnoreExceptionsCacheManagerDecorator(CacheManager decorated) {
this.decorated = decorated;
}
public Object getFromCache(String key) {
try {
return decorated.getFromCache(key);
} catch (Exception e) {
logger.log(SEVERE, "Unable to retrieve an object using key \"" + key + "\"", e);
}
return NULL;
}
public void putInCache(String key, Object o) {
try {
decorated.putInCache(key, o);
} catch (Exception e) {
logger.log(SEVERE, "Unable to store the object " + o + " using key \"" + key + "\"", e);
}
}
}
To prevent any errors in the caching system to stop the execution of any application in production, we simply need to use
IgnoreExceptionsCacheManagerDecorator
:
CacheManager cacheManager = new IgnoreExceptionsCacheManagerDecorator(new DistributedCacheManager());
The following code listing illustrates how we can use mocks (and EasyMockTemplate
) to test
IgnoreExceptionsCacheManagerDecorator
:
public class IgnoreExceptionsCacheManagerDecoratorTest {
private IgnoreExceptionsCacheManagerDecorator decorator;
private CacheManager decoratedMock;
@Before public void setUp() {
decoratedMock = createMock(CacheManager.class);
decorator = new IgnoreExceptionsCacheManagerDecorator(decoratedMock);
}
@Test public void shouldNotPropagateExceptionFromCache() {
final String key = "name";
final RuntimeException exception = new RuntimeException();
EasyMockTemplate t = new EasyMockTemplate(decoratedMock) {
@Override protected void expectations() {
expect(decoratedMock.getFromCache(key)).andThrow(exception);
}
@Override protected void codeToTest() {
try {
decorator.getFromCache(key);
} catch (Exception e) {
if (e == exception) fail("Should not propagate exception thrown by the cache");
}
assertExceptionWasLogged(exception);
}
};
t.run();
}
}
- it was easy to use a mock as the decorated object
- mocks made it easy to simulate an exception thrown by the cache
- by using mocks, we could verify the correct interaction between the decorator and the decorated object—that is, the method
get(String, Object)
fromIgnoreExceptionsCacheManagerDecorator
is callingget(String, Object)
from the decorated object
Testing decorators is, in fact, one of many possible use cases of mock testing when the interaction between two or more objects is as important as the end result of such interaction. Usage of mock testing must be determined carefully on a case-by-case basis (for example, when testing certain implementations of the Adapter pattern.) Decorators are just a special case where it is safe to introduce mocks.
Conclusion
Testing code in isolation is a challenge. Non-trivial code usually depends on collaborators that are not easy or quick to set up in tests. Developers, even the most motivated ones, can be discouraged after spending large amounts of time and energy writing, maintaining, and executing tests. To prevent testing from decreasing, mock objects provide a mechanism to test code in complete isolation, by simulating those real-world, hard-to-use, and expensive-to-use dependencies. Although mocks can simplify creation of unit tests, they are not a replacement for functional and integration tests. Mocks need to be used carefully; overusing them may introduce problems such as hidden integration issues, clutter, and duplication in test code, unnecessary code, and test fragility.
References
- Mocks Aren't Stubs by Martin Fowler
- Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
- Head First Design Patterns by Eric Freeman and Elisabeth Freeman with Kathy Sierra and Bert Bates
- JUnit in Action by Vincent Massol with Ted Husted
- Mock Objects: the Good, the Bad and the Ugly by Alex Ruiz
- FEST (Fixtures for Easy Software Testing), home of
EasyMockTemplate
Alex Ruiz is a Software Engineer in the development tools organization at Oracle.