JaVa
   

Endoscopic Testing

In the XP world, dummy objects are normally called mock objects. To our knowledge, this term was initially used in the commendable article by Mackinnon [00] about endo-testing—a pun on endoscopic surgery. We too are talking about "testing from inside" by smuggling in a test medium, namely the dummy object. In fact, the dummy logger enabled us to check for correct behavior of the log server without having to delve too deeply. Mock objects that deserve to be called by that name take us a step further than the dummy objects studied so far. Mock objects fetch the majority of the actual test code. Let's rebuild our dummy logger into a mock logger:

import java.util.*;
public class MockLogger implements Logger {
 private List expectedLogs = new ArrayList();
 private List actualLogs = new ArrayList();
 public void addExpectedLine(String logString) {
 expectedLogs.add(logString);
 }
 public void logLine(String logString) {
 actualLogs.add(logString);
}
 public void verify() {
 if (actualLogs.size() != expectedLogs.size()) {
 Assert.fail("Expected " + expectedLogs.size() +
 " log entries but encountered " +
 actualLogs.size());
 }
 for (int i = 0; i < expectedLogs.size(); i++) {
 String expectedLine = (String) expectedLogs.get(i);
 String actualLine = (String) actualLogs.get(i);
 Assert.assertEquals(expectedLine, actualLine);
 }
 }
}


The two methods, addExpectedLine() and verify(), are typical for a mock object. While the first one serves to set the expected behavior of our "client" (the log server), the second one does the actual checking for correct behavior at the end of the test. Naturally, our test class has to adapt to this new situation:

public class LogServerTest extends TestCase {
 private LogServer logServer;
 private MockLogger logger;
 protected void setUp() {
 logger = new MockLogger();
 logServer = new LogServer(logger);
 }
 public void testLoggingWithModule() {
 logger.addExpectedLine("test(0): first line");
 logServer.log(0, "first line", "test");
 logger.verify();
 }
 public void testSimpleLogging() {
 logger.addExpectedLine("(0): first line");
 logger.addExpectedLine("(1): second line");
 logger.addExpectedLine("(2): third line");
 logServer.log(0, "first line");
 logServer.log(1, "second line");
 logServer.log("third line");
 logger.verify();
 }
}


So far, we have not moved much code, so a few changes to our mock logger won't hurt:

import java.util.*;
public class MockLogger implements Logger {
 private List expectedLogs = new ArrayList();
 private List actualLogs = new ArrayList();
 public void addExpectedLine(String logString) {
 expectedLogs.add(logString);
 }
 public void logLine(String logLine) {
 Assert.assertNotNull(logLine);
 if (actualLogs.size() >= expectedLogs.size()) {
 Assert.fail("Too many log entries");
 }
 int index = actualLogs.size();
 String expectedLine = (String) expectedLogs.get(index);
 Assert.assertEquals(expectedLine, logLine);
 actualLogs.addElement(logLine);
 }
 public void verify() {
 if (actualLogs.size() < expectedLogs.size()) {
 Assert.fail("Expected " + expectedLogs.size() +
 " log entries but encountered " +
 actualLogs.size());
 }
 }
}


What we gained here appears to be subtle: one part of the verification code was moved from the verify() method to the logLine() method. This has the benefit that the feedback of a faulty log entry will occur immediately and not at the end of the test. We can see this easily, for example, by changing the line

logServer.log(0, "first line");


to

logServer.log(0, "first line wrong");


and then let the debugger track down exactly when the TestFailure exception will be thrown. In complex test cases, this exact localization of an error could shorten the debugging time noticeably. Another one of our changes was the following additional line:

Assert.assertNotNull(logLine);


Testing this important pre-condition with our "old" DummyLogger would have entailed adding it separately for each single log line. We can see that the progress achieved with mock objects versus simple dummy objects consists mainly in avoiding duplicate code. This benefit increases in line with the increasing number of mock objects we build, because the code we need to compare the expected with the actual behavior is very similar from one mock object to another so that it can be moved to separate classes. Another benefit is that mock objects enhance the communication capability of our code. Using them in the way suggested by Mackinnon et al. [00], we obtain a standardized pattern that simplifies the actual test code, thus making it more readable. Just as with the other patterns, this pattern improves communication between all who know the pattern. Our slightly modified version of the pattern for unit testing suggested in Endo-Testing [Mackinnon00] consists of the following steps that describe what a single unit test looks like:

  1. Create instances of mock objects.

  2. Set state in the mock objects.
  3. Set expectations in the mock objects.
  4. Invoke the code under test with mock objects as parameters.
  5. If applicable, use direct tests to verify state changes in the objects under test.
  6. Use verify() to verify consistency in the mock objects.

Mackinnon [00] does not list step 5; he thinks that even simple state changes are best verified through mock objects for consistency reasons. However, our experience has shown that direct polling of sheer state changes is often much simpler than building corresponding mock objects, which would be used only in this place. Once again, there is no rigid rule; whether or not the use of mock objects can improve our code remains to be seen from case to case.

The direct state test is often the simplest way, at least initially. Once the internal objects become more complex and we find code duplication in our tests, then we can gradually introduce corresponding interfaces and mock implementations. The iterative approach is the method of choice for testing, too.


JaVa
   
Comments