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