6.10 Lightweight and Heavyweight Mocks


6.10 Lightweight and Heavyweight Mocks

So far, we have seen two approaches to build a dummy object:

  1. The first approach derives the dummy object as subclass from the real implementation, for example, DummyRateProvider.

  2. In the second approach, both the real class and the mock class implement the same interface.

Although the first variant is the simpler of the two, because we do not have to implement a separate interface, it has certain risks. For example, it can easily happen that we forget to adapt the mock class when changing the signature of the real class. This means that the OUT would invoke the new method, and the test would create an unexpected failure that is normally hard to trace.

In contrast, the second variant causes additional programming work, because it first needs to extract the interface and then implement all methods in the mock class. Accordingly, any change to the signature would entail a change in (at least) three different places: in the interface itself, in the real implementation, and in all mock classes. Nevertheless, we normally prefer the second variant, because the interface also assumes a documenting function, and it reduces complexity, as in our transition from DummyPrintWriter to DummyLogger (see Section 6.3). In addition, the cost for synchronization between the interface and the implementation can be minimized by an appropriately equipped development environment or use of EasyMocks (see Section 6.5).

The UML diagram in Figure 6.2 shows the full structure of our small pattern for the introduction of mock objects. The idea behind it is that the class AbstractMock throws a NotImplementedException for all methods declared in the interface. This allows specific mock classes to extend AbstractMock and override only the methods they are interested in. More-over, common features of specific mock objects can be moved up to AbstractMock to avoid code duplication in the tests.


Figure 6.2: Schematic view of the mock objects hierarchy.

Consider the following example to better understand this approach; our logging framework should be extended so that single loggers can be replaced in the course of active operation. First of all, we need to insert the method:

 public void setLogger(Logger newLogger) {...} 

into the LogServer class. Next, we have to extend our logger interface by the method:

 public void close(); 

to ensure that a logger to be replaced gets a chance to free resources it no longer needs before it retires. Before the implementation, we write two tests for this new functionality:

 public void testSetLogger() {    MockLogger newLogger = new MockLogger();    logServer.setLogger(newLogger);    newLogger.addExpectedLine("(1): Test");    logServer.log(1, "Test");    newLogger.verify(); } public void testCloseOnSetLogger() {    logger.setCloseExpected();    logServer.setLogger(new MockLogger());    logger.verify(); } 

In the second test, we have a close() message sent to a logger before it is replaced. Subsequent extensions are now necessary in the MockLogger class:

 public class MockLogger implements Logger {    ...    private boolean closeExpected = false;    private boolean closeInvoked = false;    public void close() {       closeInvoked = true;    }    public void setCloseExpected() {       closeExpected = true;    }    public void verify() {       if (closeExpected) {          Assert.assertTrue(             "close() should have been called", closeInvoked);       }       if (actualLogs.size() < expectedLogs.size()) {          Assert.fail("Expected " + expectedLogs.size() +                      " log entries but encountered " +                      actualLogs.size());       }    } } 

The most conspicuous change was in the verify() method; we extended it by an additional verification. We can easily imagine that verify() will develop into a reservoir for all kinds of possible and impossible validation functions as we continue extending the logger functionality. Out of all these validation functions, we will probably need only one or two in each test. Although we could avoid this excessive wealth by introducing an abstract mock logger and various subclasses (e.g., TestCloseMockLogger and Test-LinesMockLogger), we would have to deal with a constantly increasing number of mock classes over the long run. And we would probably use each of these classes only once.

Java offers two little tricks to prevent this excessive reproduction of classes:

  1. If a specific mock implementation is needed for only one single test, we can create it as an anonymous class directly in the test method.

  2. If a specific mock implementation is needed within only one test class, we can create it as an internal class of the test class.

Trick 1 is suitable mainly to simulate methods that return fixed values in the test. To achieve complex validation functions in anonymous classes, we would have to perform light to medium contortions, because Java imposes some restrictions on these lightweight classes. Our Euro calculator can serve as a typical example to illustrate this technique. Its tests could do without the DummyProvider class, but each single test would become more cumbersome:

 public void testUSD2EUR() {    ExchangeRateProvider provider = new ExchangeRateProvider() {       public double getRateFromTo(String from, String to) {          return 1.1324;       }    };    double result = new EuroCalculator().valueInEuro(1.5,       "USD", provider);    assertEquals(1.6986, result, ACCURACY); } 

Trick 2 does nothing but reduce the visibility of the mock class. We leave it up to the interested reader to transfer this principle to a TestLine-MockLogger. Whether we should implement a mock class as an internal or "normal" class in a specific case depends not least on how this Java feature is supported by the development environment we use.

Finally, we should mention that there is a possibility to use the test class itself as a mock object by letting the test class implement the corresponding interface. This is a slightly modified form of the internal class approach, but without the option to inherit from an existing abstract class. This approach has been described as the self-shunt test pattern [Feathers00].

And which one of these numerous possibilities is recommended in practice? The best option is surely to stick to the XP rule, "We generally do the simplest thing that could possibly work!" [3] In our EuroCalculator example, we would begin with an anonymous class and then switch to an internal class that overrides the getRateFromTo() method in the second test. As soon as we need this class externally, or as soon as we find that a loss in manageability of the internal class neutralizes the small benefit of reduced visibility, we extract the internal class and make it a fully fledged member of our Java company.

Notice that the development of our MockLogger was similar. In the first approach, we had a DummyPrintWriter as direct subclass of PrintWriter. But then we found out that a dedicated interface (Logger) communicates the true purpose of the object much better and makes the code easier to read, although at the cost of programming a new interface and two new classes. In a subsequent step, we found that we can simplify our test by moving the actual validation from the test class to the dummy class, which promoted our DummyLogger to a MockLogger.

Finally, extending the logging framework led to an extension of the Logger interface and, during the test, to the wish to have different MockLogger classes. This is the hour of birth of our AbstractMockLogger:

 public class AbstractMockLogger implements Logger {    public static class NotImplemented extends RuntimeException {    }    public void close() {       throw new NotImplemented();    }    public void logLine(String logMessage) {       throw new NotImplemented();    } } 

We can now turn this logger into a specific mock logger object in an anonymous, internal, or real class. Once again, the iterative approach and development of our test framework replace the rigorous observance of rigid rules.

[3][Jeffries00, p. 74].




Unit Testing in Java. How Tests Drive the Code
Unit Testing in Java: How Tests Drive the Code (The Morgan Kaufmann Series in Software Engineering and Programming)
ISBN: 1558608680
EAN: 2147483647
Year: 2003
Pages: 144
Authors: Johannes Link

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net