|
From: Steve F. <sm...@us...> - 2001-09-08 16:30:42
|
Update of /cvsroot/mockobjects/mockobjects-java/doc/xdocs/papers
In directory usw-pr-cvs1:/tmp/cvs-serv14201
Added Files:
jdbc_testfirst.html
Log Message:
added jdbc paper
--- NEW FILE: jdbc_testfirst.html ---
<html>
<head>
<title>Developing JDBC applications test-first</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
<style type="text/css">
<!--
.deemphasised { color: #666666}
h1 { text-align: center; font-family: Arial, Helvetica, sans-serif; font-weight: bold}
h3 { font-family: Arial, Helvetica, sans-serif; font-style: italic; font-weight: bold; font-size: small}
.inline_code { font-family: "Courier New", Courier, mono; font-style: normal; font-size: smaller; vertical-align: middle}
p { font-family: Arial, Helvetica, sans-serif}
li { font-family: Arial, Helvetica, sans-serif }
h2 { font-family: Arial, Helvetica, sans-serif; margin-top: 3%}
-->
</style>
</head>
<body bgcolor="#FFFFFF">
<h1>Developing JDBC applications test-first</h1>
<p align="center">Steve Freeman <tt> <st...@m3...></tt></p>
<h2>Introduction</h2>
<p>Many developers find unit testing software with third-party components, such
as databases, hard to do. Usually the difficulties are because setting up the
component to ensure no dependencies between unit tests is too complicated or
slow. This paper uses JDBC, the Java database interface, to show how it is possible
to avoid using a real database at all. It also shows how an approach based on
Mock Objects can lead to more rigourous unit testing and, I believe, better
structured code than working with a real database. </p>
<p>There are two key skills for writing Mock Objects when developing code test-first.
First, decide what a test would need to verify to show that it had passed, and
add mock implementations of objects that represent those concepts. Second, do
not attempt to reproduce real behaviour in a mock object; if they are becoming
complex, revisit both the mock implementation and the code being tested to see
if there is some intermediate concept to be factored out.</p>
<h3>Note</h3>
<p>All the code in these examples and the mock objects library are available for
download from <a href="http://www.mockobjects.com">http://www.mockobjects.com</a>.
If you are unfamiliar with Mock Objects, please read the introductory paper in the
Documentation section.
</p>
<h3>The stories</h3>
<p>To help structure the examples, let's imagine that our customers have commissioned
us to write a mailing list system. Anyone can add and remove themselves from
the list, but only the list owner can see the whole list and send messages to
it. If we start by assuming that there's just one list, we have the following
tasks:</p>
<ul>
<li>anyone can add their name and email address to the list;</li>
<li>anyone who is a member of the list can remove themselves, using their email
address;</li>
<li>the owner of the list can see all the members of the list;</li>
<li>the owner of the list can send a message to all the members.</li>
</ul>
<h2>Add a member to the list</h2>
<p>We assume that access is controlled by the surrounding system and start to
develop a <span class="inline_code">MailingList</span> class to manage the list.
For a succesful insertion, we would expect certain things to happen:</p>
<ul>
<li>create a statement object with the appropriate insertion statement</li>
<li>set the parameters for the insertion statement based on the member's email
address and name</li>
<li>execute the statement exactly once to make the insertion</li>
<li>close the statement exactly once</li>
</ul>
<p>Let's write that into our first test.</p>
<pre> public void testAddNewMember() {
connection.setExpectedPrepareStatementString(MailingList.INSERTION_SQL);
statement.addExpectedSetParameters(new Object[] {EMAIL, NAME});
statement.setExpectedExecuteCalls(1);
statement.setExpectedCloseCalls(1);
<span class="deemphasised">[...]</span>
}</pre>
<p>This tells us that we will need to check assertions about the connection and
statement objects. It also pushes us towards passing the connection object through
to the mailing list object when we make the call to add a member. If we rename
our database objects to make it explicit that they are mock implementations,
the test becomes:</p>
<pre> public class TestMaililingList extends TestCaseMo {
public void testAddNewMember() throws SQLException {
<span class="deemphasised"> mockConnection.setExpectedPrepareStatementString(MailingList.INSERTION_SQL);
mockStatement.addExpectedSetParameters(new Object[] {EMAIL, NAME});
mockStatement.setExpectedExecuteCalls(1);
mockStatement.setExpectedCloseCalls(1);
</span>
mailingList.addMember(mockConnection, EMAIL, NAME);
mockStatement.verify();
mockConnection.verify();
}
<span class="deemphasised">[...]</span>
}
</pre>
<p></p>
<p>Note that <span class="inline_code">testAddNewMember() </span>declares <span class="inline_code">SQLException</span>
in its throws clause. This means that any <span class="inline_code">SQLException</span>
will be caught by the JUnit framework and reported as an error (a breakdown
in our environment). Strictly speaking, we should catch it and convert it to
a failure (a mistake in coding), but this style is lightweight and accurate
enough for many projects. </p>
<p>We set up the test in another method:</p>
<pre> public void setUp() {
private MailingList list = new MailingList();
private MockConnection mockConnection = new MockConnection();
private MockPreparedStatement mockStatement = new MockPreparedStatement();
mockConnection.setupPreparedStatement(mockStatement);
}</pre>
<p>The simplest implementation to pass this first test is just to work through
each of the expectations that we've set:</p>
<pre> public class MailingList {
public void addMember(Connection connection, String emailAddress, String name) throws SQLException {
PreparedStatement statement = connection.prepareStatement(INSERT_SQL);
statement.setString(1, emailAddress);
statement.setString(2, name);
statement.execute();
statement.close();
}
}</pre>
<p>You may have noticed that this implementation does not ensure that a statement
will always be closed; we'll get to that later.</p>
<h3>What have we learned?</h3>
<p>This first test is very localised. It tests exactly one thing, the successful
addition of a list member, and has no external dependencies, such as the need
for an installed database with its tables in the right state. In some situations,
where infrastructure decisions have not been taken, this will allow a project
to move ahead while the management are negotiating with your vendors.</p>
<p>There are two aspects to testing database code: what we say to the database,
and what the database expects to hear. This approach separates them, which makes
it easier to pinpoint errors as the system grows. Of course, we will also need
integration tests to check the whole process, but those should be more like
an acceptance test for a larger module.</p>
<p>This testing style pushes us towards passing objects around. In this case,
we need access to the connection object to verify it, so the easiest thing is
to pass it through to the mailing list along with the other parameters. In practice,
many systems hold connections in pools to improve performance, so passing through
a connection with each action is probably the right approach. We have found
that this style of test-first programming pushes us towards the right design
so often that we have come to rely on it.</p>
<h2>Add an existing member to the list</h2>
<p>What happens if someone tries to add an email address twice? After further
discussion, our customers decide that we should not accept the change and show
an error message; we decide to throw a <span class="inline_code">MailingListException</span>
when this happens. We can write a test to verify that such an exception is thrown
if there is a duplicate record.</p>
<pre> public void testAddExistingMember() throws SQLException {
mockStatement.setupThrowExceptionOnExecute(
new SQLException("MockStatment", "Duplicate",
DatabaseConstants.UNIQUE_CONSTRAINT_VIOLATED));
<span class="deemphasised"> mockConnection.setExpectedPrepareStatementString(MailingList.INSERTION_SQL);
mockStatement.addExpectedSetParameters(new Object[] {EMAIL, NAME});
mockStatement.setExpectedExecuteCalls(1);
mockStatement.setExpectedCloseCalls(1);</span>
try {
mailingList.addMember(mockConnection, EMAIL, NAME);
fail("should have thrown an exception");
} catch (MailingListException expected) {
}
<span class="deemphasised"> mockStatement.verify();
mockConnection.verify();
}</span>
</pre>
<p>The method <span class="inline_code">setupThrowExceptionOnExecute</span> stores
an exception that the mock SQL statement will throw when <span class="inline_code">execute()</span>
is called. We can force such events because our coding style allows us to pass
a mock implementation through to the code that uses the statement. We still
check that the right parameters are passed through to the connection and statement,
and that <span class="inline_code">execute</span> and <span class="inline_code">close</span>
are each called once. Our test ensures that this exception is caught and translated
into a <span class="inline_code">MailingListException</span>. If this doesn't
happen, then the code will step through from <span class="inline_code">addMember</span>
to the <span class="inline_code">fail</span> call and throw an assertion error.
Even with the extra complexity, the test is still precise and self-explanatory.</p>
<p>To pass this test, we change the implementation to:</p>
<pre><span class="deemphasised"> public class MailingList {
public void addMember(Connection connection, String emailAddress, String name)
throws MailingListException, SQLException
{
PreparedStatement statement = connection.prepareStatement(INSERT_SQL);</span>
try {
statement.setString(1, emailAddress);
statement.setString(2, name);
statement.execute();
} catch (SQLException ex) {
if (ex.getErrorCode() == DatabaseConstants.UNIQUE_CONSTRAINT_VIOLATED) {
throw new MailingListException("Email address exists");
} else {
throw ex;
}
} finally {
statement.close();
}
<span class="deemphasised"> }
}</span></pre>
<p>We have added a <span class="inline_code">finally</span> block to make sure
that the statement will always be closed, even if there are failures when using
it. Interestingly, the code now also passes another test:</p>
<pre><span class="deemphasised"> public void testPrepareStatementFailsForAdd() throws MailingListException, SQLException {</span>
mockConnection.setupThrowExceptionOnPrepare(new SQLException("MockConnection"));
mockConnection.setExpectedPrepareStatementString(MailingList.INSERT_SQL);
mockStatement.setExpectedExecuteCalls(0);
mockStatement.setExpectedCloseCalls(0);
<span class="deemphasised"> try {
list.addMember(mockConnection, EMAIL, NAME);
fail("Should have thrown exception");
} catch (SQLException expected) {
}
mockConnection.verify();
mockStatement.verify();
}</span></pre>
<p>which checks that we do not try to execute or close the statement should the
connection fail, although we still need to confirm that we made the right <span class="inline_code">prepareStatement</span>
request. </p>
<h3>What have we learned?</h3>
<p>One of the hardest things to test is the throwing and management of exceptions.
I have seen a lot of code that does not clean up resources on failure, or does
so but is too complicated. Incremental, test-first programming is very effective
at focussing a developer's attention on correct error handling by simulating
exceptions that can be expensive, or even impossible, to generate from the real
libary. Of course, how careful we should be depends on the application; we may
not need precise cleanup for a single-action utility, but we certainly do for
a long-lived server.</p>
<p>The other design point to note from this example is that I have distinguished
between mailing list exceptions (the system works, but was used incorrectly)
and SQL exceptions (there was a system failure). As the application grows, we
might want to translate those SQL exceptions into application-specific exceptions
to help us manage exception handling. So far, however, that isn't necessary.
<br>
</p>
<h2>Remove a member from the list</h2>
<p>The next task is to remove someone from the list, based on their email address.
The test for a successful removal is:</p>
<pre> public void testRemoveMember() throws MailingListException, SQLException {
mockStatement.setupUpdateCount(1);
<span class="deemphasised"> mockConnection.setExpectedPrepareStatementString(MailingList.DELETE_SQL);
mockStatement.addExpectedSetParameters(new Object[] {EMAIL});
mockStatement.setExpectedExecuteCalls(1);
mockStatement.setExpectedCloseCalls(1);</span>
list.removeMember(mockConnection, EMAIL);
<span class="deemphasised"> mockConnection.verify();
mockStatement.verify();
}</span>
</pre>
<p>Once again, we check that the right parameters are passed in and the right
methods called. A successful removal should return an update count greater than
0, so we preload a return value in the statement object. </p>
<p>We can also test removing someone who is not in the list by setting the update
count to 0, which would imply that no rows match the email address in the deletion
request. </p>
<pre> public void testRemoveMissingMember() throws SQLException {
mockStatement.setupUpdateCount(0);
<span class="deemphasised"> mockConnection.setExpectedPrepareStatementString(MailingList.DELETE_SQL);
mockStatement.addExpectedSetParameters(new Object[] {EMAIL});
mockStatement.setExpectedExecuteCalls(1);
mockStatement.setExpectedCloseCalls(1);
</span>
try {
list.removeMember(mockConnection, EMAIL);
fail("Should have thrown exception");
} catch (MailingListException expected) {
}
<span class="deemphasised"> mockConnection.verify();
mockStatement.verify();
}</span></pre>
<p>The implementation of <span class="inline_code">removeMember</span> that supports
both these tests is:</p>
<pre> public void removeMember(Connection connection, String emailAddress)
throws MailingListException, SQLException<br> {
PreparedStatement statement = connection.prepareStatement(DELETE_SQL);
try {
statement.setString(1, emailAddress);
if (statement.executeUpdate() == 0) {
throw new MailingListException("Could not find email address: " + emailAddress);
}
} finally {
statement.close();
}
}</pre>
<h3>Refactoring the tests</h3>
<p>At this point we should start to notice repetition in the unit tests. In particular,
each action on the member list requires a particular setup for the mock sql
objects, and we always verify the same two objects. We refactor these into some
helper methods:</p>
<pre> private void setExpectationsForAddMember() {
setExpectationsForPreparedStatement(MailingList.INSERT_SQL, new Object[] {EMAIL, NAME});
}
private void setExpectationsForRemoveMember() {
setExpectationsForPreparedStatement(MailingList.DELETE_SQL, new Object[] {EMAIL});
}
private void setExpectationsForPreparedStatement(String sqlStatement, Object[] parameters) {
mockConnection.setExpectedPrepareStatementString(sqlStatement);
mockStatement.addExpectedSetParameters(parameters);
mockStatement.setExpectedExecuteCalls(1);
mockStatement.setExpectedCloseCalls(1);
}
private void verifyJDBC() {
mockConnection.verify();
mockStatement.verify();
}
</pre>
<p>This allows us to simplify our existing tests and make them easier to read,
for example:</p>
<pre><span class="deemphasised"> public void testRemoveMember() throws MailingListException, SQLException {
mockStatement.setupUpdateCount(1);
</span>
setExpecationsForRemoveMember();
list.removeMember(mockConnection, EMAIL);
verifyJDBC();
}
<span class="deemphasised"> public void testRemoveMissingMember() throws SQLException {
mockStatement.setupUpdateCount(0);</span>
setExpectationsForRemoveMember();
try {
list.removeMember(mockConnection, EMAIL);
fail("Should have thrown exception");
} catch (MailingListException expected) {
}
verifyJDBC();
}</pre>
<h3> What have we learned?</h3>
<p>As the application grows, we find that the incremental costs of adding new
tests falls as we start to benefit from the unit tests we have alread written.
At first, the benefit is intellectual, we can write a new test by copying and
altering what we already have. The next stage is to refactor the tests to remove
duplication. Maintaining and refactoring tests turns out to be as important
as maintaining the code they exercise. The tests must be agile enough to follow
the code as it is grows and changes. </p>
<p>That said, we must maintain a balance here and not refactor the tests until
they become too obscure to read; this applies particularly to verifications.
In our example, the two verifiable objects are obvious enough that we can move
them into a method with a meangingful name. Elsewhere it may be better to leave
the calls to <span class="inline_code">verify </span>in each test case to make
it clear to the reader exactly what is being tested.</p>
<h2>Show the members of the list</h2>
<p>Our next requirement is to show all the members of the list. The first question
is how can we tell that we've seen everyone, so we create an object to work
through all the list members; let's call it <span class="inline_code">ListAction</span>
for now. We will set up a mock database and ask the <span class="inline_code">ListAction</span>
object to verify that it has received the values that we expect it to when called
by the <span class="inline_code">MailingList</span> object.</p>
<h3>Extracting a single row</h3>
<p>The simplest first test is for a list with one member. We should ensure that
the <span class="inline_code">ListAction</span> object receives the right member
details, that the ResultSet is asked for the right column values and that <span class="inline_code">next()</span>
is called twice. We also check the usual expectations about creating and closing
a statement and executing the query. Our first test implements exactly this:</p>
<pre> public void testListOneMember() throws SQLException {
MockSingleRowResultSet mockResultSet = new MockSingleRowResultSet();
mockStatement.setupResultSet(mockResultSet);
mockResultSet.addExpectedNamedValues(
COLUMN_NAMES,
new Object[] {EMAIL, NAME});
mockResultSet.setExpectedNextCalls(2);
mockListAction.addExpectedMember(EMAIL, NAME);
setExpectationsForListMembers();
list.applyToAllMembers(mockConnection, mockListAction);
verifyJDBC();
mockResultSet.verify();
mockListAction.verify();
}</pre>
<p>The value expectations in a <span class="inline_code">MockSingleRowResultSet</span>
serve two purposes. They are checked to verify that the client has asked for
exactly the right columns, either by name or position, and they store the values
that are returned. It is arguable that these two functions should be split,
but the pattern occurs often enough that we have implemented it in an <span class="inline_code">ExpectationMap</span>.
The <span class="inline_code">ListAction</span> class maintains expectations
about the name and email address of the members of the list. If we write enough
code to fulfil just this test, we will have a sketch of the structure of the
method: </p>
<pre> public void applyToAllMembers(Connection connection, ListAction listAction) throws SQLException {
Statement statement = connection.createStatement();
ResultSet results = statement.executeQuery(LIST_SQL);
while (results.next()) {
listAction.applyTo(results.getString("email_address"), results.getString("name"));
}
statement.close();
}</pre>
<h3>Extracting more than one member</h3>
<p>This first test proves that we can extract the right values from a valid row
in a <span class="inline_code">ResultSet</span>. Once we have a test for this,
our other tests can concentrate on other aspects, such as handling different
numbers of valid rows. We do not need to re-test extraction in every case. For
example, when testing for different numbers of list members we only need to
set expectations for the number of times we call the <span class="inline_code">listAction</span>
object, we do not need to check again that the right values have been received.
This makes tests more precise and orthogonal, and so easier to understand when
they fail. It also makes test cases and mock objects easier to write because
each one does less.</p>
<p>For example, the test for listing two members does not set any expectations
for the list member name and email address. This version does set column names
and some dummy values, but these are only necessary to avoid type casting errors
when returning values from the <span class="inline_code">ResultSet</span>; they
are also used to define the number of rows to return. The expectations we <em>do</em>
set are concerned with the number of times various methods are called.</p>
<pre> public void testListTwoMembers() throws SQLException {
MockMultiRowResultSet mockResultSet = new MockMultiRowResultSet();
mockStatement.setupResultSet(mockResultSet);
mockResultSet.setupColumnNames(COLUMN_NAMES);
mockResultSet.setupRows(TWO_ROWS);
mockResultSet.setExpectedNextCalls(3);
mockListAction.setExpectedMemberCount(2);
<span class="deemphasised"> setExpectationsForListMembers();
list.applyToAllMembers(mockConnection, mockListAction);
verifyJDBC();
mockResultSet.verify();
mockListAction.verify();</span>
}</pre>
<p>The test for a <span class="inline_code">ResultSet</span> with no rows is even
simpler, we set up no row values at all and we tell the mock <span class="inline_code">ListAction</span>
object to expect not to be called. At this point, we can also factor out setting
up the mock <span class="inline_code">ResultSet</span> into a helper method.</p>
<pre> public void testListNoMembers() throws SQLException {
MockMultiRowResultSet mockResultSet = makeMultiRowResultSet();
mockResultSet.setExpectedNextCalls(1);
mockListAction.setExpectNoMembers();
<span class="deemphasised"> setExpectationsForListMembers();
list.applyToAllMembers(mockConnection, mockListAction);
verifyJDBC();
mockResultSet.verify();
mockListAction.verify();</span>
}
</pre>
<h3>Handling failures</h3>
<p>Our simple implementation of <span class="inline_code">applyToAllMembers</span>
can support all these tests, but we know that we still have to manage exceptions.
We should write an additional test to confirm that we can cope if the <span class="inline_code">ResultSet</span>
fails. As with the test for adding an existing member, we wrap the call to the
<span class="inline_code">ListAction</span> object in a <span class="inline_code">try</span>
block and fail if the exception is not thrown. We also use the same technique
as before to tell the mock <span class="inline_code">ResultSet</span> to throw
an exception when anyone tries to get a value.</p>
<pre><span class="deemphasised"> public void testListResultSetFailure() {
MockMultiRowResultSet mockResultSet = makeMultiRowResultSet();
mockResultSet.setupRows(TWO_ROWS);</span>
mockResultSet.setupThrowExceptionOnGet(new SQLException("Mock Exception"));
mockResultSet.setExpectedNextCalls(1);
mockListAction.setExpectNoMembers();
<span class="deemphasised"> setExpectationsForListMembers();</span>
try {
list.applyToAllMembers(mockConnection, mockListMembers);
fail("Should have thrown exception");
} catch (SQLException expected) {
}
<span class="deemphasised"> mockResultSet.verify();
mockListMembers.verify();
}</span>
</pre>
<p>One significant point about this test is that it clearly defines the behaviour
of <span class="inline_code">applyToAllMembers</span> in the presence of failures.
We do not, for example, try to carry on should a record fail. Developing test-first
and publishing the tests gives users of the code a much clearer description
of how to use it than most conventional documentation.</p>
<p>Our implementation will not pass this test because it has no <span class="inline_code">finally</span>
clause, so we add it now.</p>
<pre><span class="deemphasised"> public void applyToAllMembers(Connection connection, ListAction listAction) throws SQLException {
Statement statement = connection.createStatement();</span>
try {
<span class="deemphasised"> ResultSet results = statement.executeQuery(LIST_SQL);
while (results.next()) {
listAction.applyTo(results.getString("email_address"), results.getString("name"));
}</span>
} finally {
statement.close();
}
<span class="deemphasised"> }</span>
</pre>
<p>We might also add tests to check that we correctly handle failures when the
connection cannot create a statement or the statement cannot execute the query.
For a long-lived server, this sort of error handling avoids resource leakages
that can be very difficult to find in production.</p>
<h3>Presenting the results</h3>
<p>Now we can ask a <span class="inline_code">MailingList</span> object to process
all the members of the list by passing an object of type <span class="inline_code">ListAction</span>.
We might present the results by implementing a <span class="inline_code">ConvertToMemberNode</span>
class that adds each member's details to an XML document. That rendering is
managed by another class, <span class="inline_code">ListDocument</span>, that
is passed in when starting the action.</p>
<pre> class ConvertToMemberNode implements ListAction {<br> ListDocument listDocument;
public ConvertToMemberNode(ListDocument aListDocument) {
listDocument = aListDocument;
}
public void applyTo(String email, String name) {
listDocument.startMember();
listDocument.addMemberEmail(email);
listDocument.addMemberName(name);
listDocumnet.endMember();
}
}</pre>
<p>Of course this new class should have its own set of unit tests. </p>
<h3>What have we learned?</h3>
<p>A common difficulty when testing networks of objects is the cost of setting
up the test environment. These techniques show that we can manage this by being
very precise about what each test verifies, and by providing just enough mock
implementation to get us through. Teasing out the different aspects of what
we are testing to make the tests more orthogonal has several advantages: the
tests are more precise, so errors are easier to find; smaller tests are easier
to implement and easier to read; and, the mock implementations can be more specialised
and so simpler to implement. </p>
<p>An alternative approach is to use one of the available stub JDBC implementations,
or even a scratch copy of a real database. This is a good technique when getting
started with unit testing or when retrofitting tests to an existing code base,
but the mock object approach still has some advantages: </p>
<ul>
<li>the setup is cheaper, which makes the tests easier to maintain and quicker
to run; </li>
<li>failures can happen as soon as something unexpected happens, rather than
verifying the result afterwards; </li>
<li>we can verify behaviour such as the number of times times <span class="inline_code">close()</span>
is called; and, </li>
<li>we can force unusual conditions such as SQL exceptions.</li>
</ul>
<h2>Send a message to all the members</h2>
<p>Our final requirement is to send a message to all the members of the list.
We can do this by providing another implementation of the <span class="inline_code">ListAction
</span> interface, in this case a <span class="inline_code">AddMemberToMessage</span>
class. We have to make a design choice here about how to handle any messaging
exceptions: we can either stop immediately, or collect the failures and process
them at the end; for now, we shall stop processing immediately. The <span class="inline_code">Message</span>
object is set up by the calling code, which then sends it to all the recipients.</p>
<pre>
class AddMembersToMessage implements ListAction {
Message message;
public AddMemberToMessage(Message aMessage) {
message = aMessage;
}
public void applyTo(String email, String name) throws MailingListException {
try {
message.addRecipient(RecipientType.TO,
new InternetAddress(email, name));
} catch (MessagingException ex) {
throw new MailingListException("Adding member to message", email, name, ex);
}
}
}
</pre>
<p>The <span class="inline_code">MessagingException</span> exception is checked,
so we have to handle it here or propagate it through the code. We want to keep
the <span class="inline_code">ListAction </span> interface generic, so we translate
the <span class="inline_code">MessagingException</span> to a <span class="inline_code">MailingListException</span>,
and propagate that instead. Now we have a new failure mode, so we write an additional
unit test that ensures that we clean up properly after a <span class="inline_code">MailingListException</span>.</p>
<pre><span class="deemphasised"> public void testListmembersFailure() throws SQLException {
MockMultiRowResultSet mockResultSet = makeMultiRowResultSet();
mockResultSet.setupRows(TWO_ROWS);</span>
mockListAction.setupThrowExceptionOnMember(new MailingListException());
mockResultSet.setExpectedNextCalls(1);
<span class="deemphasised"> setExpectationsForListMembers();
try {
list.applyToAllMembers(mockConnection, mockListAction);
fail("Should have thrown exception");</span>
} catch (MailingListException expected) {
<span class="deemphasised"> }
mockResultSet.verify();
mockListAction.verify();
}</span>
</pre>
<h3>Tell, don't ask</h3>
<p>This approach to test-first development has led to a solution that may appear
needlessly complex, as against just returning a collection of members from the
<span class="inline_code">MailingList</span>. Our experience, however, is that
code written this way is much more flexible. For example, if we later decide
to ignore message errors and keep sending, we can do so by collecting them in
the <span class="inline_code">AddMemberToMessage</span> class and processing
them after the message has been sent. Similarly, if our mailing list becomes
very large, we can start to process recipients in small batches and do not have
to worry about creating excessively large collections of list members. On the
other hand, if we do want to return a list of members, we can still write another
<span class="inline_code">ListAction</span> implementation that appends each
member to a collection.</p>
<h2>Summary</h2>
<p>This example shows how far you can you can isolate your unit tests from each
other and avoid external dependencies by adopting Mock Objects. More interestingly,
this approach to unit testing leads your code towards a style that is both flexible
and easy to test, including testing system exceptions. As an application grows,
common test infrastructure gets pushed into the mock implementations and the
incremental cost of adding new tests drops substantially. In our experience,
the first few tests with a complex new library, such as JDBC, can be slow to
write but the mock implementations stabilise very quickly—particularly
when using run-time proxies or an IDE to generate stub methods.</p>
<hr>
<p>© Steve Freeman, 2001</p>
<p> </p>
</body>
</html>
|