From: Steve F. <sm...@us...> - 2001-08-11 20:45:36
|
Update of /cvsroot/mockobjects/doc In directory usw-pr-cvs1:/tmp/cvs-serv2460 Modified Files: jdbc_testfirst.html Log Message: more Index: jdbc_testfirst.html =================================================================== RCS file: /cvsroot/mockobjects/doc/jdbc_testfirst.html,v retrieving revision 1.7 retrieving revision 1.8 diff -C2 -d -r1.7 -r1.8 *** jdbc_testfirst.html 2001/08/09 22:06:49 1.7 --- jdbc_testfirst.html 2001/08/11 20:45:33 1.8 *************** *** 112,117 **** <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. First we write a test:</p> <pre> public void testAddExistingMember() throws SQLException { mockStatement.setupThrowExceptionOnExecute( --- 112,118 ---- <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( *************** *** 119,126 **** DatabaseConstants.UNIQUE_CONSTRAINT_VIOLATED)); ! mockConnection.setExpectedPrepareStatementString(MailingList.INSERTION_SQL); mockStatement.addExpectedSetParameters(new Object[] {EMAIL, NAME}); mockStatement.setExpectedExecuteCalls(1); ! mockStatement.setExpectedCloseCalls(1); try { --- 120,127 ---- 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 { *************** *** 130,146 **** } ! mockStatement.verify(); mockConnection.verify(); ! } </pre> ! <p>We still want to check that we pass the right parameters through to the database, ! but this time we want to emulate the effect of writing a duplicate record. The ! call <span class="inline_code">setupThrowExceptionOnExecute</span> stores an ! exception that will be thrown when we try to execute the statement; note that ! we still need to ensure 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.</p> <p>To pass this test, we change the implementation to:</p> <pre><span class="deemphasised"> public class MailingList { --- 131,149 ---- } ! <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 { *************** *** 164,170 **** <span class="deemphasised"> } }</span></pre> ! <p>At this point we add 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 also passes this test:</p> <pre><span class="deemphasised"> public void testPrepareStatementFailsForAdd() throws MailingListException, SQLException {</span> mockConnection.setupThrowExceptionOnPrepare(new SQLException("MockConnection")); --- 167,173 ---- <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")); *************** *** 198,204 **** 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 a generic system exception ! so that we can generalize the exception handling. So far, however, that isn't ! necessary. <br> </p> <h2>Remove a member from the list</h2> --- 201,207 ---- 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> *************** *** 222,227 **** methods called. A succesful removal should return an update count greater than 0, so we preload a return value in the statement object. </p> ! <p>Next we test removing someone who is not in the list by setting the update ! count to 0:</p> <pre> public void testRemoveMissingMember() throws SQLException { mockStatement.setupUpdateCount(0); --- 225,230 ---- methods called. A succesful 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> <pre> public void testRemoveMissingMember() throws SQLException { mockStatement.setupUpdateCount(0); *************** *** 241,245 **** mockStatement.verify(); }</span></pre> ! <p>The implementation of <span class="inline_code">removeMember</span> is:</p> <pre> public void removeMember(Connection connection, String emailAddress) throws MailingListException , SQLException<br> { --- 244,249 ---- 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> { *************** *** 278,282 **** } </pre> ! <p>and our existing tests simplify to, for example:</p> <pre><span class="deemphasised"> public void testRemoveMember() throws MailingListException, SQLException { mockStatement.setupUpdateCount(1); --- 282,287 ---- } </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); *************** *** 300,312 **** <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 already have. At first, ! the benefit is intellectual, we can write a new test by altering what we have ! already done; 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 hard 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 --- 305,317 ---- <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 *************** *** 362,396 **** statement.close(); }</pre> ! <h3>What have we learned?</h3> ! <p></p> ! <p>We can test quite complicated networks of objects by being clear about what ! we want to test and being ruthless in minimising the support we provide in our ! mock implementation. There are several excellent stub implementations of the ! JDBC library but these are not necessary where we have enough control to structure ! our code appropriately. </p> ! <p>We can also make such testing much easier by factoring out the complexities ! into common support libraries.</p> <p>This first test proves that we can extract the right values from a valid row ! in a <span class="inline_code">ResultSet</span>. With the test in place we can ! simplify our other tests to concentrate on other aspects, such as handling different ! numbers of valid rows. For example, the test for listing two members is:</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); mockListMembers.setExpectedMemberCount(2); ! setExpectationsForListMembers(); list.applyToAllMembers(mockConnection, mockListMembers); ! <span class="deemphasised"> verifyJDBC(); mockResultSet.verify(); mockListMembers.verify();</span> }</pre> ! For this test, all we are concerned with is that <br> ! <br> <hr> <p>© Steve Freeman, 2001</p> --- 367,509 ---- 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">listMembers</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, 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 ! 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 ! 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); mockListMembers.setExpectedMemberCount(2); ! <span class="deemphasised"> setExpectationsForListMembers(); list.applyToAllMembers(mockConnection, mockListMembers); ! verifyJDBC(); mockResultSet.verify(); mockListMembers.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> ! 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(); ! <span class="deemphasised"> setExpectationsForListMembers(); ! ! list.applyToAllMembers(mockConnection, mockListMembers); ! ! verifyJDBC(); ! mockResultSet.verify(); ! mockListMembers.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">ListMembers</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); ! mockListMembers.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>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 { ! 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")); ! }</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>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> ! <pre> class MembersAsTableRow implements ListMembers {<br> PrintWriter writer; ! public MembersAsTableRow(PrintWriter aPrintWriter) { ! writer = aPrintWriter; ! } ! public member(String email, String name) { ! writer.print("<TR><TD>"); ! writer.print(email); ! writer.print("</TD><TD>"); ! writer.print(name); ! writer.println("</TD></TR>"); ! } ! }</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 ! 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> <hr> <p>© Steve Freeman, 2001</p> |