Lecture 8: Controllers and Mock Objects
1 An elegant idiom for testing synchronous controllers
1.1 Interactions
1.2 Scripting our controller
1.3 Streamlining the code further
6.5

Lecture 8: Controllers and Mock Objects

More text to follow; for now, the slides and code from today’s lecture is posted.

1 An elegant idiom for testing synchronous controllers

The behaviors of a synchronous controller follow a script: prompt the user, wait for a response, prompt again. Testing these interactions would be quite tedious if we had to build up the expected input and output strings all in one step: how do we remember whether this output belongs with that prompt, or not? To streamline this, we’ll use several features of Java—function objects, variadic functions, mock input and output objects—and and build up a much cleaner way to write these tests.

1.1 Interactions

First, let’s define a new interface:

/**
 * An interaction with the user consists of some input to send the program
 * and some output to expect.  We represent it as an object that takes in two
 * StringBuilders and produces the intended effects on them
 */
interface Interaction {
  void apply(StringBuilder in, StringBuilder out);
}
This interface defines the signature of a function object, whose reason for being is to represent user/program interactions.

We can define classes for the two most common interactions: printing text to the user, or responses from the user. We show this code two different ways: first as standard Java classes, and then again using Java’s new lambda syntax (see Streamlining the code further below).

/**
 * Represents the printing of a sequence of lines to output
 */
class PrintInteraction implements Interaction {
  String[] lines;
  PrintInteraction(String... lines) {
    this.lines = lines;
  }
  public apply(StringBuilder in, StringBuilder out) {
    for (String line : lines) {
      out.append(line).append("\n");
    }
  }
}
/**
 * Represents a user providing the program with  an input
 */
class InputInteraction implements Interaction {
  String input;
  InputInteraction(String input) {
    this.input = input;
  }
  public apply(StringBuilder in, StringBuilder out) {
    in.append(input);
  }
}

For example, the following array of Interactions represents two inputs to our Calculator and their corresponding outputs:

Interaction[] interactions = new Interaction[] {
  new InputInteraction("+ 3 4\n"),
  new PrintInteraction("7"),
  new InputInteraction("+ 5 7\n"),
  new PrintInteraction("12"),
  new InputInteraction("q\n")
};

But how can we use them?

1.2 Scripting our controller

Suppose we construct two initially-empty StringBuilder objects, and we feed them to each Interaction in turn:

StringBuilder sb1 = new StringBuilder();
StringBuilder sb2 = new StringBuilder();
for (Interaction i : interactions) {
  i.apply(sb1, sb2);
}
What are the contents of sb1.toString() and sb2.toString()? They’re exactly the intended input and corresponding expected output from our program. If we then feed sb1.toString() to our controller, we can drive it with our intended inputs. If we give the controller another empty StringBuilder to use as output, we can compare its value to our expected one, and thereby test our controller:

void testRun(Model model, Interaction... interactions) throws IOException {
  StringBuilder fakeUserInput = new StringBuilder();
  StringBuilder expectedOutput = new StringBuilder();

  for (Interaction interaction : interactions) {
    interaction.apply(fakeUserInput, expectedOutput);
  }

  StringReader input = new StringReader(fakeUserInput.toString());
  StringBuilder actualOutput = new StringBuilder();

  Controller controller = new Controller(model, input, actualOutput);
  controller.run();

  assertEquals(expectedOutput.toString(), actualOutput.toString());
}

(We pass in a model rather than a controller because we need to construct the controller in the test itself, using the given model.)

This sort of schematic function is called a test harness: it is the infrastructure by which we can run a variety of structurally-related tests without having to repeat ourselves. (In general, test harnesses can be much more complex...up to, say, being able to detect and run all our tests for us, and produce an easy-to-read report of the results. In other words, JUnit.) Now we merely need to say:

testRun(new Calculator(),
  new InputInteraction("+ 3 4\n"),
  new PrintInteraction("7"),
  new InputInteraction("+ 5 7\n"),
  new PrintInteraction("12"),
  new InputInteraction("q\n"));

But we can do better yet!

1.3 Streamlining the code further

The definitions for PrintInteraction and InputInteraction are clunky, full of boilerplate that would be nice to eliminate. In fact, we can. Java 8 has introduced lambda expressions that make it far easier to write function objects. Java still insists that all functions must reside in objects (unlike, say, Racket, where functions and structs were distinct), but Java has introduced syntactic sugar to make it easier to write them down. 1Syntactic sugar is new syntax that is not strictly necessary, in that you could accomplish exactly the same goal using other, more verbose syntax. But syntactic sugar is often “sweeter” to use than the alternatives.

Our first refactoring is to write a static function surrounding the calls to our constructors:

static Interaction prints(String... lines) {
  return new PrintInteraction(lines);
}

This doesn’t save much typing; in fact, it’s now longer than our original! But our next step is to use lambda syntax, and eliminate the definition of PrintInteraction altogether:

static Interaction prints(String... lines) {
  return (input, output) -> {
    for (String line : lines) {
      output.append(line).append('\n');
    }
  };
}
This function has the same signature as above, which means we can call it the same way. Internally, Java sees the syntax (...) -> {...} and automatically creates a new, anonymous class that implements the Interaction interface (which it determines is needed because of our return-type annotation), whose one and only method is defined as the body of code above. In other words, this lambda above is exactly the same as our original definition, except it’s anonymous.

We can redefine our other class, too:

static Interaction inputs(String in) {
  return (input, output) -> {
    input.append(in);
  };
}

And now we can test our controller by saying:

testRun(new Calculator(),
        inputs("+ 3 4\n"),
        prints("7"),
        inputs("+ 5 7\n"),
        prints("12"),
        inputs("q\n"));

This is about as terse and expressive as it gets!

We can define additional convenience interactions too: for example, a prompt and response:

static Interaction prompts(String prompt, String response) {
  return (input, output) -> {
    output.append(prompt).append("\n");
    input.append(response);
  }
}

or a formatted output:

static Interaction formats(String template, Object... params) {
  return (input, output) -> {
    output.append(String.format(template, params));
  }
}

The possibilities here can extend to whatever the scenario is that you’re testing. By defining these convenience interactions, it becomes much clearer to express your intended tests as a sequence of interactions, rather than as two giant strings with no delimiters to guide the way. Moreover, changing the interactions automatically changes the inputs and outputs in tandem; it’s impossible for them to become inconsistent with each other.

1Syntactic sugar is new syntax that is not strictly necessary, in that you could accomplish exactly the same goal using other, more verbose syntax. But syntactic sugar is often “sweeter” to use than the alternatives.