CS 214: Programming Languages
Spring 2009

Home|Syllabus|Schedule
<<|>>|ANTLR API|CITkit API|PolyD API|EasyMock API

Mock Objects
Interpreters, Iteration 13

The CITkit library has changed based on the work you're doing in this iteration.

  1. Get the latest version of the JAR file from SourceForge.
  2. Change the return type of createOperatorMap() to use OperatorETIR.IOperator instead of OperatorETIR.Operator. (That is, add an I!)

The acceptance tests in CIAT make sure that everything works together, including the map of operator algorithms from createOperatorMap() with a HobbesInterpreter.

In the last iteration, you removed any responsibility for filling the map of operator algorithms from HobbesInterpreter. And yet, your unit tests are still testing for this in shouldInterpretOperatorExpression() (or whatever you called it) in HobbesInterpreterTest.

Keep in mind that the tests for the operator algorithms are testing the actual computations. So the only responsibility that HobbesInterpreterTest really has is to make sure that the map of operator algorithms is working the way it should. The unit tests should make sure that just the object under test is working, not other objects.

You don't have to use the map created by HobbesInterpreterFactory.createOperatorMap(). That's the purpose of passing the map into the constructor of HobbesInterpreter!

The question is: what do you put into this map? If you put in any existing operator algorithms, you're back where you started from: testing both the interpretation of an operator expression and the computation itself. What you need is a fake operator-algorithm which is easy to control from just a test.

Let's at least commit to your map in the test:

Red bar.

You should get a null pointer exception.

Interpreting Integers and Strings

The tests that make assertions about interpreting integers and strings do not need anything in the operator-algorithm map. So there's no need to put anything into the map in setUp() method or into the test methods for integers and strings.

It's only shouldInterpretOperatorExpression() that should do anything to the map.

It's for this very reason that you're storing the map in an instance variable: so that you can control it carefully in your tests.

Putting an Operator Algorithm into the Map

You will need to add this code to shouldInterpretOperatorExpression():

myOperatorMap.put(key, value);

But what to use for key and value?

I've already told you not to use a real Operator and OperatorAlgorithm. Truth be told, it's not terrible to use real objects, but what real objects would you use? Maybe Operator.ADD and an AdditionAlgorithm? To make sure that the lookup and application of the operator algorithm are working, you'd probably have to make at least three assertions. Any fewer than three, a clever (but evil) implementer would hard code an interpretation based on your test data.

That's not bad, but it won't generalize well. (For example, so far you've had )

You could fake your own IOperator and OperatorAlgorithm: create your own subclasses of these interfaces, and use those in the test. But then you have to worry about making those fakes work properly.

What if someone else could create those fakes for you? What if they could be created on the fly? This is one the really cool things you can do in a language (like Java) that supports reflection: create new code on the fly. It's even better when there are existing library to create the code for you!

Nelson

You're going to use mock objects. The idea here is that you want objects that kind of behave like IOperators and OperatorAlgorithms, and you'd like to be able to control them yourself. Java has several mock-object libraries, jMock and EasyMock probably the most common. For various reasons, I've found EasyMock to be better.

The term "mock" here (despite my picture of Nelson to the right) should be thought of in terms of "mock turtleneck". My dictionary on my Mac gives this definition: "not real or authentic, but without the intention to deceive".

Setting Up EasyMock

You'll use a library called EasyMock.

Go to the EasyMock website, and download the latest ZIP file for EasyMock 2.4. Unzip the ZIP file, and copy the easymock.jar file into your lib directory. In Eclipse, add this JAR to the build path.

Using Mock Objects

A test using mock objects follows this algorithm pattern:

  1. Declare mock objects to be used in the test.
  2. Set expectations on the mock objects.
  3. Replay, compute, assert, and verify.

Computation and Assertion

I usually work backwards, with the computation and assertion:

assertSame("the answer", myInterpreter.interpret(
  new OperatorETIR(left, operator, right)
  ));

assertSame() uses == to make sure that the computed object is exactly the same as the expected value. assertEquals() uses equals(Object) to test equality. Use assertSame() with mock-object tests.

The expected value is any old string. Since the operator algorithm is mocked out, you'll be able to control what its computation returns. This should be a mock object, too, but the interpreter isn't ready for that yet.

left, right, and operator should all be declared as local variables, initialized to mock objects. Usually, only the object-under-test is not a mock object. Make an exception here because of the visitor pattern. The OperatorETIR is a secondary object-under-test. As long as its subobjects are mocks, you're doing good mock-object testing.

Delete the current assertions in shouldInterpretOperatorExpression(). Add the assertion from above. Fix the compiler error for assertSame(). You should have three complaints about left, right, and operator.

A Mock-Object Control

You have some compiler errors now, and you also haven't finished the last step of the mock-object test pattern. These are more easily solved with a mock object control (at least with the EasyMock library).

IMocksControl control = EasyMock.createControl();

Add this code to the beginning of the test method. (Still compilation errors.)

The control will create mocks for you as well as replay and verify the mocks.

Replay and Verify

Replaying your mocks (with a mock control) looks like this:

control.replay();

verifying like this:

control.verify();

Replay the mocks before the assertion. Verify the mocks after the assertion. (Still won't compile.)

No, I haven't told you what these do. They'll make sense after you have the rest of the code in place.

Creating Mock Objects

You need to declare local variables left, operator, and right; each one needs to be set to a new mock object. Here's what one of those looks like:

ExpressionTIR left = control.createMock(ExpressionTIR.class);

Based on the OperatorETIR constructor, left needs to be of type ExpressionTIR. So that's how left is declared. As for its initialization, the mock control is asked to create a mock using the ExpressionTIR interface.

It is critical that ExpressionTIR is an interface. Due to some language issues in Java, it's very easy creating a mock object for an interface. You could try to use IntegerETIR there instead, and it will compile, but you'll get a runtime error when you run your tests.

This is more than okay because it actually moves you in the right direction. You don't want to pick a IntegerETIR or StringETIR. The left subtree is just any old ExpressionTIR.

Add this definition of left after the declaration of control and before the control.replay(). Add similar definitions of right and operator.

The code will compile now, but the unit tests will red bar.

This explains the I- prefix you'll see on a lot of the interfaces from the CITkit and other libraries. IOperator is the interface for Operator. The more interfaces you have, the more mock-object testing you can do.

operator's type should be IOperator. This is an interface for the Operator class.

Using Mocks

The red bar is due to a NPE because when your operator is looked up in the operator map, it finds nothing. Maps return null when the key can't be found.

So you need to put your mocked operator into the operator map in your test.

myOperatorMap.put(operator, algorithm);

This uses the mocked operator you already have. algorithm is new.

Keep in mind that nothing must come between the replay, compute/assert, and verify steps. All the code you add has to come before the replay.

Add this code to the test method. Define algorithm as a mock OperatorAlgorithm. Compile, and red bar.

Although... that red bar is different! It's complaining about the assertion!

Setting an Expectation

So what's missing from the mocks? visitOperatorETIR() does two things: gets an operator algorithm from the operator-algorithm map, and apply the operator algorithm.

You've taken care of the map.

It's the operator algorithm itself that's not working. The whole point of this iteration was to make that algorithm a blank slate. It doesn't add, subtract, divide, or do any computation. You're going to mock out its behavior.

Let's expression the expectation in words:

The algorithm should compute with the left and right subtrees. This computation should return the string "the answer" (since that's what's already in the assertion).

This is expressed in EasyMock like so:

EasyMock.expect(algorithm.compute(left, right)).andReturn("the answer");

Compare the English description to the code.

Add this expectation just before the replay. Green bar!

The EasyMock.expect() method allows you to say: "This method should be called on my mock object with these arguments." The chained andReturn() allows you to specify what value that invocation will return.

Replay

When a mock object is created, it's in a "recording mode". All of the expectations you set with EasyMock.expect() are recorded on the mock object. After you replay the mock (with control.replay()), the mock moves into a "playback mode". Only the expectations are allowed, and when the expected methods are called, the prescribed return values are returned.

Comment out the one expectation. Red bar. Explore the exception that's the cause of the red bar.

Uncomment the expectation. Green bar.

Comment out the control.replay(). Red bar. Puzzle at the exception.

Uncomment the replay. Green bar.

From the NPE that's thrown, it's not at all clear that a control.replay() is missing. The moral of the story is to always check that your mock-object tests have a replay just before the compute/assert.

Verify

The verify step is to make sure that all of the expectations were actually triggered. Let's add an expectation that your computation code isn't satisfying.

The purpose of getPunctuation() is to get the punctuation used for an operator, like "+" for Operator.ADD.

Add another expectation that getPunctuation() will be called on operator and return some String (your choice). Red bar. Explore the triggering exception.

Now what would happen if you didn't verify the mocks?

Comment out the control.verify(). Green bar!

Yikes! You are still protected in that the computation cannot do more than you expect, but it can do less.

Uncomment the verify step. Red bar. Delete the unnecessary expectation. Green bar.

Acceptance Tests

Actually:

Green bars all around.

The CIAT tests should work, and they're providing a very important service for you. They make sure that the operator-algorithm map from createOperatorMap() is being used by HobbesInterpreter. Of course, it doesn't test this directly, but you exercise all of the operators in those tests, and the other way they'd work through the driver is if the right map were getting into the right interpreter.

"But, You've Got to Be Kidding Me?"

Take a look at the test method. How many lines of code did you have before? More? Less?

Since the test was already working, the benefits of this switch aren't immediately obvious. There's more code now, and it takes a bit more to read it all.

However, you only need to make this one assertion now. There's no way to tweak the algorithm in visitOperatorETIR() to hard code some values and still get the test to pass. The mock objects prevent this kind of hard coding.

It's also eased a cognitive burden of remembering what a specific operator algorithm computes.

Most importantly, it's made the test very, very general. As more operators are added, there not even a way to change this test to deal with those operators! It's not the responsibility of this test.

Also, and more importantly, the left and right subtrees are mocked. In a future iteration, you'll need to manipulate those subtrees, and as mocks they'll be much easier to manipulate. And that's where the size of this method as it stands now will win out.