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
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.
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:
@CUT
documents the class under test, for future uses of the
refactoring on the same test class.@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.@RunWith(JMock.class)
names the mock framework to be used for the test class
(see below).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.
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.
@Test
and @Before
annotations (see above); this means that it does not work
with Java 1.4 and JUnit 3.new MovieLister(new ColonDelimitedMovieFinder("movies1.txt"))
is therefore not allowed.