|
From: Steve F. <sm...@us...> - 2001-08-11 23:51:19
|
Update of /cvsroot/mockobjects/doc
In directory usw-pr-cvs1:/tmp/cvs-serv12398
Modified Files:
jdbc_testfirst.html
Log Message:
Added Introduction and Send Message To Members
Index: jdbc_testfirst.html
===================================================================
RCS file: /cvsroot/mockobjects/doc/jdbc_testfirst.html,v
retrieving revision 1.8
retrieving revision 1.9
diff -C2 -d -r1.8 -r1.9
*** jdbc_testfirst.html 2001/08/11 20:45:33 1.8
--- jdbc_testfirst.html 2001/08/11 23:51:16 1.9
***************
*** 18,25 ****
<p align="center">Steve Freeman <tt> st...@m3...</tt></p>
<h2>Introduction</h2>
! <p>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>
--- 18,34 ----
<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>
! <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>
***************
*** 30,36 ****
</ul>
<h2>Add a member to the list</h2>
! <p>We assume that access control is managed by the surrounding system and start
! to develop a MailingList class to manage the list. For a succesful insertion,
! we would expect the certain things to happen:</p>
<ul>
<li>create a statement object with the appropriate insertion statement</li>
--- 39,45 ----
</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 the certain things to happen:</p>
<ul>
<li>create a statement object with the appropriate insertion statement</li>
***************
*** 85,94 ****
<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>
--- 94,102 ----
<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>
***************
*** 206,211 ****
</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>
<pre> public void testRemoveMember() throws MailingListException, SQLException {
mockStatement.setupUpdateCount(1);
--- 214,219 ----
</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>
<pre> public void testRemoveMember() throws MailingListException, SQLException {
mockStatement.setupUpdateCount(1);
***************
*** 247,251 ****
both these tests is:</p>
<pre> public void removeMember(Connection connection, String emailAddress)
! throws MailingListException , SQLException<br> {
PreparedStatement statement = connection.prepareStatement(DELETE_SQL);
try {
--- 255,259 ----
both these tests is:</p>
<pre> public void removeMember(Connection connection, String emailAddress)
! throws MailingListException, SQLException<br> {
PreparedStatement statement = connection.prepareStatement(DELETE_SQL);
try {
***************
*** 261,265 ****
<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 factor these into some
helper methods:</p>
<pre> private void setExpectationsForAddMember() {
--- 269,273 ----
<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() {
***************
*** 309,313 ****
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
--- 317,321 ----
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
***************
*** 362,368 ****
Statement statement = connection.createStatement();
ResultSet results = statement.executeQuery(LIST_SQL);
! while (results.next()) {
listMembers.member(results.getString("email_address"), results.getString("name"));
! }
statement.close();
}</pre>
--- 370,376 ----
Statement statement = connection.createStatement();
ResultSet results = statement.executeQuery(LIST_SQL);
! while (results.next()) {
listMembers.member(results.getString("email_address"), results.getString("name"));
! }
statement.close();
}</pre>
***************
*** 376,381 ****
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, which 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
--- 384,389 ----
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
***************
*** 447,450 ****
--- 455,463 ----
}</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>
***************
*** 468,472 ****
<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 an <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>
--- 481,485 ----
<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>
***************
*** 475,479 ****
writer = aPrintWriter;
}
! public member(String email, String name) {
writer.print("<TR><TD>");
writer.print(email);
--- 488,492 ----
writer = aPrintWriter;
}
! public void member(String email, String name) {
writer.print("<TR><TD>");
writer.print(email);
***************
*** 483,488 ****
}
}</pre>
! Of course this new class should have its own set of unit tests.<br>
! <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
--- 496,501 ----
}
}</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
***************
*** 506,509 ****
--- 519,586 ----
<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">ListMembers</span>
+ interface, in this case a <span class="inline_code">AddMembersToMessage</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 {
+ Message message;
+ public AddMembersToMessage(Message aMessage) {
+ message = aMessage;
+ }
+ public void member(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">ListMembers</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());
+
+ mockResultSet.setExpectedNextCalls(1);
+ <span class="deemphasised"> setExpectationsForListMembers();
+
+ try {
+ list.applyToAllMembers(mockConnection, mockListMembers);
+ fail("Should have thrown exception");</span>
+ } catch (MailingListException expected) {
+ <span class="deemphasised"> }
+ mockResultSet.verify();
+ mockListMembers.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">AddMembersToMessage</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>
+ <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>
<hr>
<p>© Steve Freeman, 2001</p>
|