This book is about the techniques of using tests to guide the development of object-oriented software, not about specific technologies. To demonstrate the techniques in action, however, we've had to pick some technologies for our example code. For the rest of the book we're going to use Java, with the JUnit 4 and jMock 2 testing frameworks. If you're using something else, we hope that we've been clear enough so that you can apply these ideas in your environment.
In this chapter we briefly describe the programming interfaces for these two frameworks, just enough to help you make sense of the code examples we've written. If you already know how to use them, then you can skip this chapter.
We use JUnit 4 as our Java test harness. It's
small, open-source, bundled with many IDEs and can be downloaded from
www.junit.org if your IDE does not include it. The essence of
JUnit is that it uses reflection to walk the structure of a class and run
whatever it can find in that class that represents a test. That sentence
will make more sense if we show an example, here's a test that exercises a
Catalog class that manages a collection of Entries:
public class CatalogTest {
final Catalog catalog = new Catalog();
@Test public void containsAnAddedEntry() {
Entry entry = new Entry("fish", "chips");
catalog.add(entry);
assertTrue(catalog.contains(entry));
}
@Test public void indexesEntriesByName() {
Entry entry = new Entry("fish", "chips");
catalog.add(entry);
assertEquals(entry, catalog.entryFor("fish"));
assertNull(catalog.entryFor("missing name"));
}
}JUnit treats any method annotated with
@Test as a test case. Test methods must
have no parameters. In this case, the CatalogTest defines two tests, called
“containsAnAddedEntry” and
“indexesEntriesByName”.
To run each test, JUnit creates a new instance of the class and calls the relevant test method. Each test method runs in a separate object, so tests are isolated from one another: any changes that a test method makes to the object are not visible to other tests.
A JUnit test invokes the object under test and then makes assertions about the results, usually using assertion methods defined by JUnit which generate useful error messages when they fail.
CatalogTest, for example, uses three of JUnit's assertions:
assertTrue asserts that an expression is true;
assertNull asserts that an object reference is null;
and assertEquals asserts that two values are
equal. When it fails, assertEquals reports the expected
and actual values that were compared.
The @Test annotation supports an optional parameter,
expected, that declares that the test case should
throw an exception. The test fails if it does not
throw an exception or if it throws an exception of a different
type.
For example, the following test checks that a Catalog throws an
IllegalArgumentException when two entries are added with the same
name:
@Test(expected=IllegalArgumentException.class)
public void cannotAddTwoEntriesWithTheSameName() {
catalog.add(new Entry("fish", "chips");
catalog.add(new Entry("fish", "tartare sauce");
}A test fixture is the fixed state that exists at the start of a test. A test fixture ensures that a test is repeatable: every time a test is run it starts in the same state and should therefore produce the same results. A fixture may be set up before the test runs and torn down after it has finished.
The fixture of a JUnit test is managed by the class that defines
the test and stored in the object's instance variables. All tests
defined in the same class start with the same fixture, and may then
modify that fixture as they run. For CatalogTest,
the fixture is the empty Catalog object held
in its catalog field.
The fixture is usually set up by field initialisers. It can also be set up by the constructor of the test class or instance initialiser blocks. JUnit also lets you identify methods that set up and tear down the fixture with annotations. JUnit will run all methods annotated with @Before before running the tests, to set up the fixture, and those annotated by @After after it has run the test, to tear down the fixture. Many JUnit tests do not need to explicitly tear down the fixture because it is enough to let the JVM garbage collect any objects created when it was set up.
For those of you working with .Net, NUnit
behaves differently. It keeps the instance of the test class between calls to
tests methods, so the fixture must be reset in its [Setup] and
[TearDown] methods. This can be a surprise for developers moving
between the two frameworks.
For example, all the tests in CatalogTest initialise the
catalog with the same entry. The common initialisation can be moved into
a field initialiser and @Before method:
public class CatalogTest {
final Catalog catalog = new Catalog();
final Entry entry = new Entry("fish", "chips");
@Before public void fillTheCatalog() {
catalog.add(entry);
}
@Test public void containsAnAddedEntry() {
assertTrue(catalog.contains(entry));
}
@Test public void indexesEntriesByName() {
assertEquals(equalTo(entry), catalog.entryFor("fish"));
assertNull(catalog.entryFor("missing name"));
}
@Test(expected=IllegalArgumentException.class)
public void cannotAddTwoEntriesWithTheSameName() {
catalog.add(new Entry("fish", "tartare sauce");
}
}The way that JUnit reflects on a class to find tests and then runs
those tests is controlled by a test runner. The
runner used for a class can be configured with the
@RunWith annotation. JUnit provides a
small library of test runners. For example, the
Parameterised test runner lets you write data
driven tests in which the same test methods are run for many different
data values returned from a static method.
As we'll see below, the jMock library uses a custom test runner to automatically verify mock objects at the end of the test, before the test fixture is torn down.
Hamcrest is a framework for writing declarative match criteria. While not a testing framework itself, Hamcrest is used by several testing frameworks, including JUnit, jMock and Window Licker, which we use in the worked example for end-to-end testing.
A Hamcrest Matcher reports whether an object matches some criteria and can describe its criteria. For example, this code creates Matchers that matche if given strings that contain a given substring, and uses them to make some assertions.
String s = "yes we have no bananas today";
Matcher<String> containsBananas = new StringContains("bananas");
Matcher<String> containsMangos = new StringContains("mangos");
assertTrue(containsBananas.matches(s));
assertFalse(containsMangos.matches(s));Matchers are not usually instantiated directly. Instead, Hamcrest provides static factory methods for all of its matchers to make code that creates matchers more readable. For example
assertTrue(containsString("bananas").matches(s));
assertFalse(containsString("mangos").matches(s));In practice, however, we use Matchers in combination with JUnit's
assertThat, which uses the Matcher's
self-describing structure when an assertion fails to generate a
message that makes it clear exactly what went wrong. We can rewrite the
assertions as:
assertThat(s, containsString("bananas"));
assertThat(s, not(containsString("mangos"));The second assertion demonstrates one of Hamcrest's most useful
features: defining new criteria by combining existing matchers. The
not method is a factory function that creates a
matcher that reverses the sense of any matcher passed to it. Matchers are
designed so that when they're combined, both the code and the failure
messages are self-explanatory. For example, if we change the second assertion
to fail:
assertThat(s, not(containsString("bananas"));the failure report is:
java.lang.AssertionError:
Expected: not a string containing "bananas"
got: "Yes, we have no bananas"
This feature makes assertThat a convenient alternative to
writing new assertion methods. Instead of writing code to explicitly check
a condition and generate informative error messages, we can
pass a matcher expression to assertThat and let it
do the work.
Hamcrest is also user extensible. If we need to check a specific condition,
we can write a new matcher, by implementing the Matcher
interface and an appropriately-named factory method, and the result will
combine seamlessly with the existing matcher expressions.
jMock 2 plugs into JUnit (and other test frameworks) with support for the Mock Objects testing style introduced in Chapter 2, Test-Driven Development with Objects. jMock creates mock objects dynamically, so you don't have to write your own implementations of the types you want to mock. It also provides a high-level API for specifying how the object test should invoke the mock objects it interacts with, and how the mock objects will behave in response.
The core concepts of the jMock API are the Mockery, Mock Objects and Expectations. The Mockery represents the context of the object under test, its neighbouring objects; Mock Objects stand in for the real neighbours of the object under test while the test runs; and, Expectations describe how the object under test should invoke its neighbours during the test.
Again, let's start with an example to show how these fit together.
The test below asserts that a
AuctionMessageTranslator will parse a given message
text to generate an auctionClosed()
event—for now just concentrate on the structure, the test will turn
up again in context in Chapter 8, Getting ready to bid.
@RunWith(jMock.class)public class AuctionMessageTranslatorTest { private final Mockery context = new JUnit4Mockery();
private final AuctionEventListener listener = context.mock(AuctionEventListener.class);
private final AuctionMessageTranslator translator = new AuctionMessageTranslator(listener);
@Test public void notfiesAuctionClosedWhenCloseMessageReceived() { Message message = new Message(); message.setBody("SOLVersion: 1.1; Event: CLOSE;");
context.checking(new Expectations() {{
exactly(1).of (listener).auctionClosed();
}}); translator.processMessage(UNUSED_CHAT, message);
![]()
} }
The | |
The test creates the Mockery. Because this is a JUnit 4 test, it creates a JUnit4Mockery, which throws the right type of exception to report test failures to JUnit 4. By convention, jMock tests store the Mockery in a variable named "context", because it represents the context of the object under test. | |
The test uses the Mockery to create a mock
| |
The test instantiates the object under test, an
| |
The test sets up further objects that will be used in the test. | |
The test then tells the Mockery how the translator should invoke its neighbours during the test by defining a block of Expectations. The Java syntax we use to do this is obscure, so if you can bear with us for now we explain it in more detail in Appendix C, jMock 2 Cheat Sheet. | |
This is the significant line in the test, its one expectation.
It says that, during the action, we expect the listener's
| |
This is the call to the object under test, the outside event that triggers the behaviour we want to test. It passes a raw CLOSE message to the translator which should, the test says, make the translator call auctionClose() once on the listener. The Mockery will check that the mock objects are invoked as expected while the test runs and fail the test immediately if they are invoked unexpectedly. | |
Note that the test does not require any assertions. This is quite common in Mock Object tests. |
The example above specifies one very simple expectation. jMock's expectation API is very expressive. It lets you precisely specify:
the minimum and maximum number of times an invocation is expected;
whether an invocation is expected (the test should fails if it is not received) or merely allowed to happen (the test should pass if it is not received);
the parameter values, either given literally or constrained by Hamcrest matchers;
ordering constraints with respect to other expectations; and,
what should happen when the method is invoked: a value to return, exception to throw, or any other behaviour.
jMock's expectation API tries to make the expectations as easy to read as possible and uses some unusual Java coding practices to do so. An expectation block stands out from the test code surrounding it to make a clear distinction between code that describes how objects should be invoked and the code that actually invokes objects and tests the results. The code within an expectation block acts like a little declarative language designed purely for describing expectations.
jMock is an example of the technique we will discuss in “Composition Code Describes System Behaviour”. An expectation line, such as
oneOf(o).doSomething(with(any(String.class)); will(returnValue(20));
implements an expectation by creating multiple objects and plugging then together, which it then feeds to the Mockery. During the test, the Mockery will pass any calls that are invoked to the expectation which will then decide whether to accept the invocation or fail. We find that this two-level style, a “language” layer that assembles objects in an “implementation” layer, gives us the benefits of (in our eyes) readable code that is also easy to extend.
There's more to the jMock API which we don't have space for in this chapter, we'll describe more of its features in Appendix C, jMock 2 Cheat Sheet, and through examples in the rest of the book. What really matters, however, is not the implementation we happened to come up with, but its underlying concepts and motivations. We will do our best to make them clear.
Copyright © 2008 Steve Freeman and Nat Pryce