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> |