|
From: Steve F. <sm...@us...> - 2001-08-27 09:41:19
|
Update of /cvsroot/mockobjects/doc
In directory usw-pr-cvs1:/tmp/cvs-serv23393
Modified Files:
jdbc_testfirst.html
Log Message:
Intro, conclusion and some fixes
Index: jdbc_testfirst.html
===================================================================
RCS file: /cvsroot/mockobjects/doc/jdbc_testfirst.html,v
retrieving revision 1.9
retrieving revision 1.10
diff -u -r1.9 -r1.10
--- jdbc_testfirst.html 2001/08/11 23:51:16 1.9
+++ jdbc_testfirst.html 2001/08/27 09:41:15 1.10
@@ -5,25 +5,38 @@
<style type="text/css">
<!--
.deemphasised { color: #666666}
-h1 { text-align: center; font-family: Arial, Helvetica, sans-serif}
-h3 { font-family: Arial, Helvetica, sans-serif; font-style: italic; font-weight: normal; font-size: medium}
-h2 { font-family: Arial, Helvetica, sans-serif; font-style: normal; font-weight: normal; line-height: 1.5}
-.inline_code { font-family: "Courier New", Courier, mono; font-style: italic; font-size: smaller}
+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>
+<p align="center">Steve Freeman <tt> <st...@m3...></tt></p>
<h2>Introduction</h2>
-<p>Many developers find unit testing third-party components such as databases
- tricky 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>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 <tt>http://www.mockobjects.com</tt>. 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
@@ -40,13 +53,13 @@
<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 the certain things to happen:</p>
+ 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 once to make the insertion</li>
- <li>close the statement once</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() {
@@ -77,11 +90,12 @@
}
</pre>
<p></p>
-<p>Note that <span class="inline_code">testAddNewMember</span> declares SQLException
- in its throws clause. This means that any SQLException 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>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();
@@ -103,12 +117,16 @@
<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 succesful
+<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. Of course, we
- will need an integration test elsewhere in the system, but this approach separates
- the two aspects (what we say to the database, and what the database expects
- to hear) which makes it easier to find errors as the system grows. </p>
+ 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,
@@ -214,7 +232,7 @@
</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 succesful removal is:</p>
+ The test for a successful removal is:</p>
<pre> public void testRemoveMember() throws MailingListException, SQLException {
mockStatement.setupUpdateCount(1);
@@ -230,10 +248,11 @@
}</span>
</pre>
<p>Once again, we check that the right parameters are passed in and the right
- methods called. A succesful removal should return an update count greater than
+ 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.</p>
+ 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);
@@ -314,7 +333,7 @@
<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
+ 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>
@@ -326,14 +345,14 @@
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 collect
- list members; let's call it <span class="inline_code">ListMembers</span> for
- now. We will set up a mock database and ask the <span class="inline_code">ListMembers</span>
+ 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">ListMembers</span> object receives the right member
+ 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>
@@ -346,31 +365,31 @@
COLUMN_NAMES,
new Object[] {EMAIL, NAME});
mockResultSet.setExpectedNextCalls(2);
- mockListMembers.addExpectedMember(EMAIL, NAME);
+ mockListAction.addExpectedMember(EMAIL, NAME);
setExpectationsForListMembers();
- list.applyToAllMembers(mockConnection, mockListMembers);
+ list.applyToAllMembers(mockConnection, mockListAction);
verifyJDBC();
mockResultSet.verify();
- mockListMembers.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">listMembers</span> class maintains expectations
+ 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, ListMembers listMembers) throws SQLException {
+<pre> public void applyToAllMembers(Connection connection, ListAction listAction) throws SQLException {
Statement statement = connection.createStatement();
ResultSet results = statement.executeQuery(LIST_SQL);
while (results.next()) {
- listMembers.member(results.getString("email_address"), results.getString("name"));
+ listAction.applyTo(results.getString("email_address"), results.getString("name"));
}
statement.close();
}</pre>
@@ -380,7 +399,7 @@
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">listMembers</span>
+ 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
@@ -389,7 +408,7 @@
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 do
+ 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();
@@ -400,31 +419,31 @@
mockResultSet.setExpectedNextCalls(3);
- mockListMembers.setExpectedMemberCount(2);
+ mockListAction.setExpectedMemberCount(2);
<span class="deemphasised"> setExpectationsForListMembers();
- list.applyToAllMembers(mockConnection, mockListMembers);
+ list.applyToAllMembers(mockConnection, mockListAction);
verifyJDBC();
mockResultSet.verify();
- mockListMembers.verify();</span>
+ 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">ListMembers</span>
+ 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);
- mockListMembers.setExpectNoMembers();
+ mockListAction.setExpectNoMembers();
<span class="deemphasised"> setExpectationsForListMembers();
- list.applyToAllMembers(mockConnection, mockListMembers);
+ list.applyToAllMembers(mockConnection, mockListAction);
verifyJDBC();
mockResultSet.verify();
- mockListMembers.verify();</span>
+ mockListAction.verify();</span>
}
</pre>
<h3>Handling failures</h3>
@@ -432,7 +451,7 @@
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">ListMembers</span> object in a <span class="inline_code">try</span>
+ <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>
@@ -442,7 +461,7 @@
mockResultSet.setupThrowExceptionOnGet(new SQLException("Mock Exception"));
mockResultSet.setExpectedNextCalls(1);
- mockListMembers.setExpectNoMembers();
+ mockListAction.setExpectNoMembers();
<span class="deemphasised"> setExpectationsForListMembers();</span>
try {
@@ -461,12 +480,12 @@
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, ListMembers listMembers) throws SQLException {
+<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()) {
- listMembers.member(results.getString("email_address"), results.getString("name"));
+ listAction.applyTo(results.getString("email_address"), results.getString("name"));
}</span>
} finally {
statement.close();
@@ -478,21 +497,21 @@
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>We can now 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">ListMembers</span>.
- In a servlet, we might present the results by implementing a <span class="inline_code">MembersAsTableRow</span>
- class that writes out each member as a row in an HTML table; the <span class="inline_code">PrintWriter</span>
- would be passed through from the request's <span class="inline_code">ServletResponse</span>.</p>
-<pre> class MembersAsTableRow implements ListMembers {<br> PrintWriter writer;
- public MembersAsTableRow(PrintWriter aPrintWriter) {
- writer = aPrintWriter;
- }
- public void member(String email, String name) {
- writer.print("<TR><TD>");
- writer.print(email);
- writer.print("</TD><TD>");
- writer.print(name);
- writer.println("</TD></TR>");
+<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>
@@ -520,19 +539,19 @@
</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">ListMembers</span>
- interface, in this case a <span class="inline_code">AddMembersToMessage</span>
+ 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 ListMembers {
+ class AddMembersToMessage implements ListAction {
Message message;
- public AddMembersToMessage(Message aMessage) {
+ public AddMemberToMessage(Message aMessage) {
message = aMessage;
}
- public void member(String email, String name) throws MailingListException {
+ public void applyTo(String email, String name) throws MailingListException {
try {
message.addRecipient(RecipientType.TO,
new InternetAddress(email, name));
@@ -544,25 +563,25 @@
</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">ListMembers</span> interface generic, so we translate
+ 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>
- mockListMembers.setupThrowExceptionOnMember(new MailingListException());
+ mockListAction.setupThrowExceptionOnMember(new MailingListException());
mockResultSet.setExpectedNextCalls(1);
<span class="deemphasised"> setExpectationsForListMembers();
try {
- list.applyToAllMembers(mockConnection, mockListMembers);
+ list.applyToAllMembers(mockConnection, mockListAction);
fail("Should have thrown exception");</span>
} catch (MailingListException expected) {
<span class="deemphasised"> }
mockResultSet.verify();
- mockListMembers.verify();
+ mockListAction.verify();
}</span>
</pre>
<h3>Tell, don't ask</h3>
@@ -571,17 +590,23 @@
<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">AddMembersToMessage</span> class and processing
+ 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 excessive 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">ListMembers</span> implementation that adds each member
- to a collection.</p>
+ 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>
-<h3>Notes</h3>
-All the code in these examples and the mock objects library are available for
-download from <tt>http://www.mockobjects.com</tt><br>
+<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>
|