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 interfaces, 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 interfaces, for the sole purpose of mock creation (trying to avoid the problems related to class-based mocks.) Typical examples include creation of interfaces 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 interfaces 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 Servlets 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.

Cache

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 Exceptions. 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 Exceptions 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) from IgnoreExceptionsCacheManagerDecorator is calling get(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

Alex Ruiz is a Software Engineer in the development tools organization at Oracle.