The Create Mock Objects (CMO) Refactoring

Purpose

to automatically replace objects on which an object under test depends with mocks; to allow independent testing of an object with as little extra effort as possible

System requirements

Rationale

Unit tests are supposed to test a single program unit, in OOP usually a class. If the class under test (CUT) depends on other classes that cannot be assumed to be correct, failure of a test case cannot be ascribed unequivocally to the CUT. Also, the other classes may have properties or requirements that make them unsuited for unit testing, for instance availability of a database connection or the database's content, expectation of user input, or insufficient performance. In these cases, the objects on which instances of a CUT depend should be replaced by mock objects which simulate (or mock) the behaviour required by the test cases.

How it works

To get an idea of what theCMO refactoring does, consider Martin Fowler's example for Dependency Injection, and assume that the following test class has been added:

public class MovieListerTest {
     
      MovieLister ml;
      MovieFinder mf;
 
      @Before
      public void setUp() throws Exception {
            mf = new ColonDelimitedMovieFinder("movies1.txt");
            ml = new MovieLister(mf);
      }
 
      @Test
      public void testMovieLister() {
            assertNotNull(ml);
      }
 
      @Test
      public void testMoviesDirectedBy() {
            Movie[] movies = ml.moviesDirectedBy("me");
            assertTrue(movies.length == 0);
      }
}

Now assume that ColonDelimitedMovieFinder has not itself been tested thoroughly, or is unavailable, or lacks a suitable database, or something else which prevents it from being used during the testing of MovieLister. In this case, the instance of ColonDelimitedMovieFinder should be replaced by a mock object. This is where our CMO refactoring tool comes in.

To start the CMO refactoring, select the corresponding entry of the test class's context menu in the Package Explorer:

Then enter the CUT and select the methods to be searched for the introduction of collaborating objects (which are the candidates for replacement through mock objects; usually, this includes the set up and all test methods; note that this step requires use of @Test and @Before annotations to work):

Based on this information, the CMO refactoring tool inspects the selected methods for the creation of objects of other than the CUT (the collaborators) and presents them (in this case only one) to the developer:

Selecting mf and pressing Finish and then OK refactors the code of MovieListerTest as follows:

@org.mockCollaborators.annotations.CUT("MovieLister")
@org.mockCollaborators.annotations.DoMockCollaborators
 
@RunWith(JMock.class)
public class MovieListerTest {
     
      private Mockery context = new JUnit4Mockery() {
            {
                  setImposteriser(ClassImposteriser.INSTANCE);
            }
      };
      static final boolean collaboratorsAreMocked = MovieListerTest.class
                  .isAnnotationPresent(DoMockCollaborators.class);
      private MovieFinder mock_mf_setUp;
      MovieLister ml;
      MovieFinder mf;
 
      @Before
      public void setUp() throws Exception {
            mf = new ColonDelimitedMovieFinder("movies1.txt");
            // Mock-objects for collaborator simulation
            mock_mf_setUp = context.mock(MovieFinder.class);
           
            // Inject mock-objects or real collaborators into CUT-instance
            if (collaboratorsAreMocked)
                  ml = new MovieLister(mock_mf_setUp);
            else
                  ml = new MovieLister(mf);
           
      }
 
      @Test
      public void testMovieLister() {
            // TODO Specify the behavior of the mock-objects created in the fixture
           
            assertNotNull(ml);
      }
 
      @Test
      public void testMoviesDirectedBy() {
            // TODO Specify the behavior of the mock-objects created in the fixture
           
            Movie[] movies = ml.moviesDirectedBy("me");
            assertTrue(movies.length == 0);
      }
}
 

A few words of explanation:

  1. The annotation @CUT documents the class under test, for future uses of the refactoring on the same test class.
  2. The annotation @DoMockCollaborators works as a compiler switch deciding over whether mocking is switched on or off. Its presence is stored in the Boolean static final variable collaboratorsAreMocked which serves as a conditional for the replacement of collaborators with mocks.
  3. The annotation @RunWith(JMock.class) names the mock framework to be used for the test class (see below).
  4. TODO tags are introduced in each test case to remind one of the necessary specification of behaviour of mock objects for the given test case. This code varies greatly from test case to test case, and also from mock framework to mock framework, and is currently not generated automatically.

Dependency on Dependency Injection

Applicability of mocking depends critically on the ability to exchange objects on which an object under test depends. Practically, this means that if dependencies are hard-coded, introduction of mock objects requires changes of the CUT only for the purpose of testing and — worse still — these changes must be undone once the testing has ended.

Therefore, the example from dependency injection has not been chosen by coincidence: testing with mock objects is greatly facilitated if objects on which instances of the CUT depend are injected using this technique. In fact, our CMO refactoring requires constructor or setter injection for all objects that are to be replaced by mocks.

Selecting the mock framework to be used

The CMO refactoring tool is prepared to support all mock frameworks for which a corresponding plugin to CMO exists. The standard installation of CMO comes with plugins for jMock 2.2.0 and 2.4.0; others can be created using the template described here.

To select a mock framework, go to the preferences page of Eclipse and choose Create Mock Objects. Then pick the desired framework from the list and enter the directory in which the library is located.

Depending on the mock framework selected, the handling plugin expects certain libraries in the given directory. For the jMock 2.2.0 plugin, these are jmock-2.2.0.jar, jmock-junit4-2.2.0.jar, jmock-legacy-2.2.0.jar, cglib-nodep-2.1_3.jar, objenesis-1.0.jar, hamcrest-core-1.1.jar, and hamcrest-library-1.1.jar. They can be downloaded here.

Limitations

new MovieLister(new ColonDelimitedMovieFinder("movies1.txt"))

is therefore not allowed.

Installation

Publications

Team