The number of components posted on TopCoder Design and Development challenges seems to increase almost every week. While more components means more variety, opportunity, and prize money (all good things), the increased number of components can lead to challenges for developers. When the component you're working on is dependent on another component that is also currently in development (or vice versa) it can make effective unit testing very difficult. The point of unit testing, of course, is to focus on one individual unit of software at a time to ensure that each piece performs its task correctly. The problem, though, is that these units don't exist in a vacuum. If the component on which your project depends is not yet ready, you will need to simulate it in some way. This is where mock objects come in. Wikipedia defines mock objects, or "mocks," as "stub-like objects that implement no logic of their own and are used to replace the parts of the system with which the unit under test interacts." If mock objects are "stub-like," how are they actually different from stubs? Distinguishing mocks from stubs can be tricky, and there are a host of articles and blog entries on the topic (some of which you can explore for yourself, below). For our purposes, it's easiest to understand stubs as static code that gives you one particular response that you expect; mock objects, on the other hand, are dynamically generated and illustrate the range of available responses in an interaction. Using a mock generally requires manually creating a mock-up implementation of the classes needed for testing. This can be a big effort, as you will need to implement each of the methods of the mock-ups as well as invent some mechanism to check the usage of the mock-up classes. In this article we'll look into an alternative to this labor-intensive approach. By relying on so-called "Mock Object" libraries, the process of creating mock-ups can be semi-automated, with the mock-up classes created in run time instead of compile time. This approach is backed with various so-called "Mock Objects" libraries. In particular, I'll look at EasyMock, a free open-source software tool that was the first dynamic Mock Object generator. While the examples below are all based on the Java version, a .NET version of EasyMock and specialized .NET libraries such as Rhino Mocks are available as well. Mock objects in practice The best way to demonstrate the value of Mock Objects is through a simple example, in which we compare them to manually coded mock-ups. In this example, let's assume we have two components posted for development -- one called "Guitars," the other called "Guitarists." The "Guitars" component defines the "Guitar" interface as: interface Guitar { void play(); }The "Guitarists" component defines the "Guitarist" interface as: interface Guitarist { void playGuitar(Guitar guitar); }It also has an implementation of this interface, defined as: public class CoolGuitarist implements Guitarist { void playGuitar(Guitar guitar) { guitar.play(); } }We want to guarantee that the CoolGuitarist.playGuitar() method will really call the Guitar.play() method, so we need to write some unit tests. Here's where we run into a problem, though, because we don't have any Guitar implementations yet for testing. Let's go ahead and code a mock-up implementation: \ class MockGuitar implements Guitar { private int timesPlayed = 0; public int getTimesPlayed() { return timesPlayed; } public void play() { timesPlayed++; } }Next, we code our unit tests with a method like this: public void testGuitaristPlay() { // Create mock-up object MockGuitar guitar = new MockGuitar(); // Create object to test Guitarist guitarist = new CoolGuitarist(); // Execute method to test guitarist.playGuitar(guitar); // Check whether the method did its work correctly assertEquals(1, guitar.getTimesPlayed()); }Let's see how this test method can be rewritten using Mock Objects library, in particular the EasyMock 2.2 library. public void testGuitaristPlay() { // Create mock-up object Guitar guitar = createMock(Guitar.class); // Create object to test Guitarist guitarist = new CoolGuitarist(); // Record the sequence of calls, the mock object should expect guitar.play(); // Replay the sequence of calls replay(guitar); // Execute method to test guitarist.playGuitar(guitar); // Verify the sequence of calls verify(guitar); } While the test method using the mock objects library is a bit longer, it does take fewer lines of code than manually coding the mock class implementation. The use of mock objects also makes things clearer for the people reading the tests (if they are familiar with the technique), as there is no need to look into the implementation of mock classes to understand any assumptions the developer made about the classes' behavior. Advanced techniques interface Guitar { boolean isTuned(); void tune() throws StringsTornException; void play(); }The CoolGuitarist class will then need to be changed to something like: public class CoolGuitarist { void playGuitar(Guitar guitar) throws StringsTornException { if (!guitar.isTuned()) { guitar.tune(); } guitar.play(); } }The Guitarist interface will need to have its playGuitar() method signature updated as well, to indicate that it can throw StringsTornException. The test case will need to test for the new behavior, as follows: public void testGuitaristPlay() { // Create mock-up object Guitar guitar = createMock(Guitar.class); // Create object to test Guitarist guitarist = new CoolGuitarist(); // Record the sequence of calls, the mock object should expect // Specify that method call should return "false" value expect(guitar.isTuned()).andReturn(false); guitar.tune(); guitar.play(); // Replay the sequence of calls replay(guitar); // Execute method to test guitarist.playGuitar(guitar); // Verify the sequence of calls verify(guitar); }For exceptions the following can be written: public void testGuitaristPlayFailure() { // Create mock-up object Guitar guitar = createMock(Guitar.class); // Create object to test Guitarist guitarist = new CoolGuitarist(); // Record the sequence of calls, the mock object should expect // Specify that method call should return "false" value expect(guitar.isTuned()).andReturn(false); expect(guitar.tune()).andThrow(new StringsTornException()); // Replay the sequence of calls replay(guitar); try { // Execute method to test guitarist.playGuitar(guitar); fail("StringsTornException should have been thrown"); } catch (StringsTornException e) { // Exception was expected } // Verify the sequence of calls verify(guitar); } The method returned by the expect() method provides even more flexibility in addition to that shown above. Beyond the andReturn() and andThrow() methods, it also provides the times() method, which allows you to specify how many method calls are expected. For example, expect(guitar.isTuned()).andReturn(true).times(3), specifies that the isTuned() method should be expected to be called three times and always return "true" value. The methods can be chained to an even further extent to specify different return values for the same call. For example, expect(guitar.isTuned()).andReturn(true).times(3).andReturn(false) will specify that the method should be expected to be called three times returning "true" value and one time returning "false" value. There are even more useful features provided by the EasyMock library. For more information, I'd encourage you to check out its documentation. Conclusions Most of the time, though, using mock objects will help you streamline and accelerate unit testing. EasyMock and similar mock object libraries are easy enough to learn and to use in the unit tests for TopCoder components and, in most cases, their use will result in a significant decrease in code amount and an increase in code clarity. To learn more, check out some of the references below. References:
|
|