From: Steve F. <sm...@us...> - 2002-08-10 18:11:02
|
Update of /cvsroot/mockobjects/no-stone-unturned/doc/xdocs In directory usw-pr-cvs1:/tmp/cvs-serv19276/doc/xdocs Modified Files: doc-book.xml Added Files: servlets_3.xml Log Message: ported servlets_3.xml --- NEW FILE: servlets_3.xml --- <chapter> <title>Test-driven servlets (iii)</title> <section> <title>Introduction</title> <para> So far, we've seen how to start writing a simple servlet from scratch and how to extend it incrementally. The implementation code is pretty clean now, but we still have two sets (plain and CSV) of very similar tests which suggests that we haven't pushed the refactoring as far as we could. To explain what I mean, take another look at one of our tests. </para> <programlisting> public void testAddTwoIntegers() throws ServletException, IOException { MockHttpServletRequest mockRequest = createMockRequest("3", "5", "/add"); mockResponse.setExpectedContentType("text/plain"); calculatorServlet.doGet(mockRequest, mockResponse); assertPageContains("3 /add 5", "Response page calculation"); assertPageContains("Result is 8", "Response page result"); }</programlisting> <para> This checks several things at once: that the parameters are pulled from the request correctly, that the calculation produces the right result, and that the calculation and its result are displayed correctly. The subtraction test also checks the same things but with a different calculation. There's a lot of duplication here; for example, we have eight tests, each of which exercises the parameter extraction—which seems excessive to me. There's something to be said for leaving some repetition in test code (unlike production code) to make it more readable, but that should be repetition of test implementation not of test functionality. With out current tests, we would have to work quite hard to make sure we only break one test at a time if, say, we wanted to change the calculation logic. Test-Driven Development allows you to plan no further ahead than you can see, but you can only get away with it if you keep your codebase clean. We need to fix the duplication in the test suite. </para> </section> <!-- Introduction --> <section> <title>Testing the writers</title> <section> <title>The first test</title> <para> I haven't quite sorted out exactly what is duplicated, so let's start by adding tests to check the calculation writers directly. For now, we'll bundle the tests for both writers into a <classname>CalculationWriterTest</classname> class. We start by writing a test for plain output of a succesful calculation. First, we already know from the servlet how the writer will be called. </para> <programlisting> MockHttpServletResponse mockResponse = new MockHttpServletResponse(); PlainCalculationWriter plainWr = new PlainCalculationWriter(); plainWr.initialize(mockResponse); plainWr.binaryOp("3", "an op", "5"); plainWr.result(66);</programlisting> <para>Next, we expect that the writer will set the content type in the response.</para> <programlisting> mockResponse.setExpectedContentType("text/plain");</programlisting> <para> Finally, we want to check that the response has had its content type set and that the writer constructs the page contents correctly; we'll copy <function>assertPageContains()</function> from the servlet test class. </para> <programlisting> assertPageContains("3 an op 5", "Plain text calculation"); assertPageContains("Result is 66", "Plain text result");</programlisting> <para>Put together, the whole test looks like this.</para> <programlisting> public class CalculationWriterTest extends TestCase { private MockHttpServletResponse mockResponse = new MockHttpServletResponse(); private PlainCalculationWriter plainWr = new PlainCalculationWriter(); &elipsis; public void testWriteResultPlain() throws ServletException, IOException { mockResponse.setExpectedContentType("text/plain"); plainWr.initialize(mockResponse); plainWr.binaryOp("3", "an op", "5"); plainWr.result(66); assertPageContains("3 an op 5", "Plain result calculation"); assertPageContains("Result is 66", "Plain result"); } }</programlisting> &greenbar; <para> The test passes? Good. The test says that, given an HTTP response and some inputs, a <classname>PlainCalculationWriter</classname> will set the response content type and write out those inputs in the given format. </para> <para> Wait a minute, what's all this about <literal>"an op"</literal> for the operation and <literal>66</literal> for the result? That's not a proper calculation. Exactly! We're not testing the calculation, we're testing how a succesful calculation would be displayed. The calculation writer really doesn't care what the calculation is, it just needs some values of the correct type. I could use values from a real calculation, but I like using silly values just to emphasise the point to anyone reading the test. In case you missed it, let me make the point again: <emphasis>this test is not about performing a calculation, it's about turning a calculation into text</emphasis>. </para> </section> <section> <title>The other tests</title> <para>While you're digesting that nugget, we'll fill in the other tests.</para> <programlisting> public class CalculationWriterTest &elipsis; private CvsCalculationWriter cvsWr = new CvsCalculationWriter(); public void testWriteErrorPlain() throws ServletException, IOException { mockResponse.setExpectedContentType("text/plain"); plainWr.initialize(mockResponse); plainWr.binaryOp("3", "an op", "5"); plainWr.error("an error"); assertPageContains("3 an op 5", "Plain error calculation"); assertPageContains("Error: an error", "Plain error"); } public void testWriteResultCsv() throws ServletException, IOException { mockResponse.setExpectedContentType("text/csv"); csvWr.initialize(mockResponse); csvWr.binaryOp("3", "an op", "5"); csvWr.result(66); assertPageContains("3,an op,5,66", "Csv result"); } public void testWriteErrorCsv() throws ServletException, IOException { mockResponse.setExpectedContentType("text/csv"); csvWr.initialize(mockResponse); csvWr.binaryOp("3", "an op", "5"); csvWr.error("an error"); assertPageContains("3,an op,5,,an error", "Csv error"); }</programlisting> <para> That's interesting. We only need two tests for each kind of writer: one for a result and one for an error. These tests don't need to include all the other conditions in the servlet tests, such as not being an integer, because those decisions are taken outside the writer. If we could avoid testing the writers in the servlet tests, we would have less duplication in the tests. Maybe we're on to something. </para> </section> <section> <title>Managing tests</title> <para> Now we have two test classes, <classname>CalculationWriterTest</classname> and <classname>CalculationServletTest</classname>. At the time of writing, the standard graphical JUnit runners will only run one <classname>TestCase</classname> at a time, but we must run all the tests after each change to make sure we haven't broken anything. To avoid picking out each test class by hand, we can write a third test class, <classname>AllTests</classname>, to gather together the test cases. I usually end up writing an <classname>AllTests</classname> class for each package. In this case, it looks like: </para> <programlisting> public class AllTests extends TestCase { public AllTests(String name) { super(name); } public static Test suite() { TestSuite result = new TestSuite(); result.addTestSuite(CalculatorServletTest.class); result.addTestSuite(CalculationWriterTest.class); return result; } }</programlisting> <para> We add a line to the <function>suite()</function> method for each test class we want to include. The test runner will work through the test suite generated by <classname>AllTests</classname> and run every test in every test class that it can find. If we run <classname>AllTests</classname> now we should have twelve tests passing, eight for the servlet and four for the calculation writers. </para> </section> <section> <title>What have we learned?</title> <para> We've tested a writer in isolation by applying the same principle here as we did to testing the calculation servlet. The tests avoid setting up the target object's real environment by providing a mock implemention of everything it interacts with and then calling it directly. For the servlet, that means calling <function>doGet()</function> with a fake HTTP request and response. In this test, we call <function>initialize()</function> with a fake HTTP response, and call <function>binaryOp()</function>, <function>result()</function>, and <function>error()</function> with dummy values. </para> <para> What we're beginning to see is a separation of concerns in the unit tests. The new tests for the calculation writers do not depend on any other part of the application. That's why I make a point of using silly values in this sort of test, I want to be clear about exactly what I'm testing. As you build up a code base this way, you start to see advantages. When you suddenly discover that a helper class is useful elsewhere, it comes ready parcelled with its own set of unit tests which also give you a good description of how to talk to it. More interestingly, as we shall soon see, this kind of testing forces you to clarify the conceptual edges within your code. Done right, the flex points you put in your program for testing turn out to be the flex points you need as your program evolves. </para> </section> </section> <!-- Testing the writers --> <section> <title>Adding some indirection</title> <section> <title>Introduction</title> <para> Now we've tested the calculation writers, we don't need to test them via the servlet tests. In fact, our commitment to removing duplication says that we <emphasis>shouldn't</emphasis> test them via the servlet tests. If we remove the writer logic from the <classname>CalculatorServlet</classname>, its tests can concentrate on parameter extraction and the calculation itself. One way of making that happen would be to substitute a fake calculation writer that just returns canned responses, but our current implementation won't let us do that; the calculation writers are created directly in the servlet, so there's nowhere to make the substitution. We need to slip in a level of indirection to give us the flexibility we need. First we need to do some work on our type sytem. </para> </section> <section> <title>The CalculationResponse interface</title> <para>Take another look at the body of our servlet method.</para> <programlisting> protected void doGet( &elipsis; CalculationWriter calcWriter = createCalculationWriter(request); calcWriter.<emphasis>initialize</emphasis>(response); calcWriter.<emphasis>binaryOp</emphasis>(value1, operation, value2); try { calcWriter.<emphasis>result</emphasis>(binaryOp(operation, asInt(value1), asInt(value2))); } catch (IllegalArgumentException ex) { calcWriter.<emphasis>error</emphasis>(ex.getMessage()); } }</programlisting> <para> None of the method names of the <classname>CalculatorWriter</classname> has anything to do with writing or printing, they're all about reacting to the various stages of performing a calculation. Perhaps our class is misnamed? I don't think so, because our writer classes <emphasis>are</emphasis> about printing values. Instead, we can use a Java interface to make this distinction clear. The new interface describes what to do with a calculation that the servlet implements, so let's call it a <classname>CalculationResponse</classname>. </para> <programlisting> public class CalculatorServlet &elipsis; protected void doGet( &elipsis; <emphasis>CalculationResponse calcResponse = createCalculationResponse(request);</emphasis> <emphasis>calcResponse</emphasis>.initialize(response); <emphasis>calcResponse</emphasis>.binaryOp(value1, operation, value2); try { <emphasis>calcResponse</emphasis>.result(binaryOp(operation, asInt(value1), asInt(value2))); } catch (IllegalArgumentException ex) { <emphasis>calcResponse</emphasis>.error(ex.getMessage()); } } } public abstract class CalculationWriter <emphasis>implements CalculationResponse</emphasis> &elipsis; <emphasis>public interface CalculationResponse { void initialize(HttpServletResponse response) throws IOException; void binaryOp(String value1, String operation, String value2); void result(int result); void error(String message); }</emphasis></programlisting> <para> That didn't hurt <emphasis>very</emphasis> much and it's taught us something about our code. Now we have a protocol for the servlet to talk to other objects about the calculations it performs. </para> </section> <section> <title>A new factory</title> <para> The thing that stops us from substituting a dummy <classname>CalculationResponse</classname> is that that we don't have a good place to hook in a different implementation, so we'll introduce a factory class to give us the level of indirection we need. First we create an empty factory class and move the <function>createCalculationResponse()</function> method across to it. Now we can instantiate the new factory and call its <function>create()</function> method. </para> <programlisting> public class CalculatorServlet &elipsis; protected void doGet( &elipsis; CalculationResponse calcResponse = <emphasis>new CalculationResponseFactory().create(request);</emphasis> &elipsis; <emphasis>public class CalculationResponseFactory { public CalculationResponse create(HttpServletRequest request) { if ("csv".equalsIgnoreCase(request.getParameter("format"))) { return new CsvCalculationWriter(); } else { return new PlainCalculationWriter(); } } }</emphasis></programlisting> <para> Taking another look at our new class, I'm not really happy with calling it a factory. It doesn't read well and, one day, we might not be returning new instances each time. We just want to describe an object that will start the process of responding, so let's call it a <classname>Responder</classname>. Similarly, in English we "respond to" a request, so let's change the method name. </para> <programlisting> <emphasis>public class Responder { public CalculationResponse respondTo(HttpServletRequest request) {</emphasis> &elipsis; public class CalculatorServlet &elipsis; protected void doGet( &elipsis; CalculationResponse calcResponse = <emphasis>new Responder().respondTo(request);</emphasis> &elipsis;</programlisting> <para> Finally, we don't need to create a new instance of the factory every time so we make it a field of the servlet, initialised when the object is created. </para> <programlisting> public class CalculatorServlet &elipsis; <emphasis>private Responder responder = new Responder();</emphasis> protected void doGet( &elipsis; CalculationResponse calcResponse = <emphasis>responder</emphasis>.respondTo(request);</programlisting> </section> <section> <title>New tests</title> <para> Our new <classname>Responder</classname> class describes how to set up a response object depending on an HTTP request. As with the calculation writers, we can test this new object separately. If we do that we have a motivation for removing duplication by pullling it out of the servlet tests. The new tests for this implementation are pretty simple, we just have to prove that it returns the right kind of writer for a given format; and we do remember to add the new test class to <classname>AllTests</classname>. </para> <programlisting> public class ResponderTest extends TestCase { private Responder responder = new Responder(); public void testCsvTextRequested() { MockHttpServletRequest mockRequest = makeMockRequest("csv"); assertEquals("Should be csv writer", CsvCalculationWriter.class, responder.respondTo(mockRequest).getClass()); } public void testUnknownFormatRequested() { &elipsis; public void testNoFormatRequested() { &elipsis; private MockHttpServletRequest makeMockRequest(final String format) { return new MockHttpServletRequest() { public String getParameter(String key) { if ("format".equals(key)) return format; throw new AssertionFailedError("Incorrect parameter " + key); } }; } }</programlisting> </section> <section> <title>Replacing the Responder</title> <para> We need one more indirection, and then we'll be ready. We need to change the behaviour of the <classname>Responder</classname> so that it can return a fake <classname>CalculationResponse</classname>. Once again, I don't want to leave test implementations in production code, so I need to make another substitution, which means that I need another interface. <classname>Responder</classname> is an effective name, so let's change the name of the class to <classname>TextResponder</classname> (it's about responders that write text) and extract <classname>Responder</classname> as an interface. </para> <programlisting> <emphasis>public interface Responder { CalculationResponse respondTo(HttpServletRequest request); }</emphasis> public class TextResponder <emphasis>implements Responder</emphasis> { &elipsis; public class CalculatorServlet extends HttpServlet { private Responder responder = new <emphasis>TextResponder();</emphasis> &elipsis;</programlisting> </section> <section> <title>What have we learned?</title> <para> We've now hidden the implementation and creation of the <classname>CalculationResponse</classname> objects from the rest of the servlet. This means that the two components, servlet and calculation response, can be changed independantly of each other, all they have in common is a protocol, a convention for communicating, defined by the interfaces <classname>Responder</classname> and <classname>CalculationResponse</classname>. </para> <para> We're beginning to see a pattern for removing duplication from tests. If we're testing a class through a containing class, write more tests to exercise it directly; this makes the duplication clear. Then convert the communication with the class to interfaces; this allows us to substitute a stub implementation in the container. (Unfortunately, Java requires two interfaces, one for getting hold of an intstance and one to talk to it, but a good refactoring development environment makes this easy to do.) Now we can test the containing class against a fake implementation of the other class, which makes the two sets of tests independant of each other. I'll show you what I mean. </para> </section> </section> <!-- Adding some indirection --> <section> <title>Splitting out the tests</title> <section> <title>Don't test the formatting</title> <para>Let's take another look at our servlet addition test.</para> <programlisting> public void testAddTwoIntegers() throws ServletException, IOException { MockHttpServletRequest mockRequest = createMockRequest("3", "5", "/add"); mockResponse.setExpectedContentType("text/plain"); calculatorServlet.doGet(mockRequest, mockResponse); assertPageContains("3 /add 5", "Response page calculation"); assertPageContains("Result is 8", "Response page result"); }</programlisting> <para> We can fold in the changes we've made. First, we set up a fake calculation response, responder, and HTTP request. We tell the servlet to use the mock <classname>Responder</classname> when it needs to create a calculation response, the mock <classname>Responder</classname> to return the mock <classname>CalculationResponse</classname> when <classname>respondTo()</classname> is called, and we set up the HTTP request as if the user had typed in an addition. </para> <programlisting> public void testAddTwoIntegers() throws ServletException, IOException { <emphasis>MockCalculationResponse mockCalcResponse = new MockCalculationResponse(); MockResponder mockResponder = new MockResponder(mockCalcResponse); calculatorServlet.setResponder(mockResponder);</emphasis> MockHttpServletRequest mockRequest = createMockRequest("3", "5", "/add");</programlisting> <para> Then we tell the various mock objects what to expect will happen to them. We tell the <classname>Responder</classname> to expect to be called with a given HTTP request, and we tell the <classname>CalculationResponse</classname> to expect to be called with a given HTTP response, binary operation, and result. </para> <programlisting> <emphasis>mockResponder.setExpectedRequest(mockRequest); mockCalcResponse.setExpectedInitialize(mockResponse); mockCalcResponse.setExpectedBinaryOp("3", "/add", "5"); mockCalcResponse.setExpectedResult(8);</emphasis></programlisting> <para> Then we call the servlet with our mock HTTP request and response, and ask the mock responder and mock calculation response to verify that our expectations have been met—that they were called with the right input values. </para> <programlisting> calculatorServlet.doGet(mockRequest, mockResponse); <emphasis>mockResponder.verify(); mockCalcResponse.verify();</emphasis> }</programlisting> <para> This is straightforward. The mock responder has the responsibility to check that it's called with the incoming HTTP request, and the mock calculation response will check that it's called with the right calculation values. So where do we check the output page? We don't, at least not here. That's covered in the <classname>CalculationResponse</classname> tests, all we need to check now is that the servlet talks to a <classname>CalculationResponse</classname> correctly given the values in the HTTP request.. Let me make the point again: <emphasis>this test is not about turning a calculation into text, it's about performing a calculation.</emphasis> </para> </section> <section> <title>A Mock Calculation Response</title> <para> Now we know where we want to go, but we haven't yet implemented the mock responder and calculation response that will make the test work. Normally I would start with the <classname>MockResponder</classname> because it's the first class that we come across in the test but, for now, I'll fix it up so that it just returns whichever <classname>CalculationResponse</classname> we set up in its constructor. That will get us through the first part of the servlet. I'll concentrate on the <classname>MockCalculationResponse</classname> because it's a little more complex so it will lead us in some interesting directions, but the concept is exactly the same. Let's look again at our use of the <classname>MockCalculationResponse</classname> as it's passed from the test to the servlet. </para> <programlisting> <emphasis>CalculatorServletTest</emphasis> <emphasis>mockCalcResponse</emphasis>.setExpectedInitialize(mockResponse); <emphasis>mockCalcResponse</emphasis>.setExpectedBinaryOp("3", "/add", "5"); <emphasis>mockCalcResponse</emphasis>.setExpectedResult(8); <emphasis>CalculatorServlet</emphasis> <emphasis>calcResponse</emphasis>.initialize(response); <emphasis>calcResponse</emphasis>.binaryOp(value1, operation, value2); try { <emphasis>calcResponse</emphasis>.result(binaryOp(operation, asInt(value1), asInt(value2))); } catch (IllegalArgumentException ex) { <emphasis>calcResponse</emphasis>.error(ex.getMessage()); } <emphasis>CalculatorServletTest</emphasis> <emphasis>mockCalcResponse</emphasis>.verify();</programlisting> <para> First, we need to test that the right response object is passed through. We store the expected value and check it against the object that the servlet passes through. </para> <programlisting> public class MockCalculationResponse implements CalculationResponse { <emphasis>private HttpServletResponse expectedResponse;</emphasis> public void initialize(HttpServletResponse response) throws IOException { <emphasis>Assert.assertEquals("CalculationResponse.response", expectedResponse, response);</emphasis> } public void setExpectedInitialize(HttpServletResponse response) { <emphasis>expectedResponse = response;</emphasis> } &elipsis; }</programlisting> <para> This test does not yet ensure that we have actually called the <function>initialize()</function> method. We can't be certain of this until after we've finished with the servlet, so we add a check in the <function>verify()</function> method that the HTTP response was set. </para> <programlisting> public class MockCalculationResponse implements CalculationResponse { private HttpServletResponse expectedResponse; <emphasis>private HttpServletResponse actualResponse;</emphasis> public void initialize(HttpServletResponse response) throws IOException { Assert.assertEquals("CalculationResponse.response", expectedResponse, response); <emphasis>actualResponse = response;</emphasis> } public void verify() { <emphasis>Assert.assertNotNull("CalculationResponse.response", actualResponse);</emphasis> } &elipsis; }</programlisting> </section> <section> <title>Further expectations</title> <para> So far, we've checked converting the servlet inputs and displaying the request. Next, we need to check that the calculation generates the right result. The obvious solution is to do the same as the previous tests with an <token>int</token>. We use an <classname>Integer</classname> object for the actual result, so that we can tell whether it's been set or not. </para> <programlisting> public class MockCalculationResponse implements CalculationResponse { <emphasis>private int expectedResult; private Integer actualResult;</emphasis> &elipsis; <emphasis>public void result(int result) { Assert.assertEquals("CalculationResponse.result", expectedResult, result); actualResult = new Integer(result); } public void setExpectedResult(int result) { expectedResult = result; }</emphasis> public void verify() { &elipsis; <emphasis>Assert.assertNotNull("CalculationResponse.result", actualResult);</emphasis> } }</programlisting> <para> Unfortunately, this makes the tests for bad input fail because they set an error rather than a result. </para> <screen> Testcase: testUnknownOperation took 0 sec FAILED junit.framework.AssertionFailedError: CalculationResponse.result at tdd.MockCalculationResponse.verify(MockCalculationResponse.java:66) at tdd.CalculatorServletTest.testUnknownOperation(CalculatorServletTest.java:66)</screen> &redbar; <para> If we change <varname>expectedResult</varname> to be an <classname>Integer</classname> object, we can tell when no expectation has been set because it will be null. If we also change <function>verify()</function> to use <function>assertEquals()</function>, it will pass when we need it to: when no expectation has been set and <function>result()</function> has not been called, both values will be <function>null</function>. I know that this is a repetition of the test in <function>result()</function>, but it'll do for now and I have a solution ready in the next chapter. Here's the code: </para> <programlisting> public class MockCalculationResponse implements CalculationResponse { private <emphasis>Integer</emphasis> expectedResult; private Integer actualResult; &elipsis; public void result(int result) { <emphasis>actualResult = new Integer(result);</emphasis> Assert.assertEquals("CalculationResponse.result", expectedResult, <emphasis>actualResult</emphasis>); } public void setExpectedResult(int result) { expectedResult = <emphasis>new Integer(result);</emphasis> } public void verify() { &elipsis; <emphasis>Assert.assertEquals("CalculationResponse.result", expectedResult, actualResult);</emphasis> } }</programlisting> &greenbar; <para>Now the tests pass, and we can do the same to check the error message.</para> </section> <section> <title>Fewer tests</title> <para> I expect you're wondering about the benefit from all this effort. Now we can halve the number of tests for the serlvet because we don't have to test the same behaviour with both text formats, plain and csv. We're relying on the calculation writer tests to check that side of things. All we're testing here is that, given the right parameters, we deliver the right values to a <classname>CalculationResponse</classname>. For example, an error test looks like: </para> <programlisting> public void testUnknownOperation() throws ServletException, IOException { mockRequest = createMockRequest("3", "5", "bad op"); mockResponder.setExpectedRequest(mockRequest); mockCalcResponse.setExpectedInitialize(mockResponse); mockCalcResponse.setExpectedBinaryOp("3", "bad op", "5"); mockCalcResponse.setExpectedError("unknown operation bad op"); calculatorServlet.doGet(mockRequest, mockResponse); mockResponder.verify(); mockCalcResponse.verify(); }</programlisting> <para> I could carry on with this, but the rest of the code is much the same, so I'll leave you to work it out for yourselves—or take a peek at the published examples. </para> </section> <section> <title>What have we learned</title> <para> The changes in this section have taken a while, but they're important because they show how to divide up code along its conceptual boundaries, <emphasis>and</emphasis> how to divide up the testing that goes with it. Now we can test a calculation separately from the way it's rendered on a page. This kind of rigour helps to keep your codebase nimble because it minimizes the ripple effects of making a change. We can change the the calculations or the way we print them independantly of each other. We don't have to write tests that exercise the calculation functionality over and over again, when what we really want to get at is the output formatting. </para> <para> In this section we're also beginning to see how we might use expectations. Now we have a <classname>MockCalculationResponse</classname> that can handle errors or results, and knows when to fail appropriately. What we're doing in practice is refactoring some of our assertions out of the tests into the test infrastructure; we're removing duplication. For example, every servlet test needs to check that the <classname>CalculationResponse</classname> is initialized with an <classname>HttpServletResponse</classname>. Now that assertion is implemented once in the <classname>MockCalculationResponse</classname> and called from each test. If we ever need to use a <classname>CalculationResponse</classname> elsewhere, we have the test infrastructure ready. </para> </section> </section> <!-- Splitting out the tests --> <section> <title>Summary</title> <para> This chapter is about just how far you can, or <emphasis>must</emphasis>, go when refactoring. It's not enough to refactor the code, you have to bring the tests with you as well. If you push the tests hard so that they really are independant of each other, they will force you to clarify the structure of your production code. In the servlet example, we now have a name, <classname>CalculationResponse</classname> for the interaction between a calculation and the component that presents it. When one of those changes, we'll know exactly where to slot in the new functionality. </para> <para> Strictly speaking, the code I've produced is more complicated than some test-driven developers would like. I've added a factory interface, <classname>Responder</classname>, so that I can substitute a different <classname>CalculationResponse</classname>. I'm prepared to live with this because, in production, it's just a matter of where to hang the response creation method. In return, I get to strip out the duplication from my tests. I'm not committing Test-Driven Heresy and designing ahead, because each change is still driven by a test, but I've found over time that the flex points I put in to make testing easier tend to be the flex points I need to add new functionality. </para> <para> There's another, more subtle point. When I write new code this way, I find that my classes are more focussed, with clean interfaces between them. Each class tends to end up doing just one thing well—like the <classname>CalculationWriter</classname>s that take in calculation details and send out text—so they're less dependant on external state. This makes the codebase easier to work with when I'm trying to avoid duplication; I'm more likely to be able to get at existing functionality to reuse it than in most conventionally written code that I've seen. </para> <para> You might well be thinking that it takes far too much effort to write this kind of test, particularly when starting the initial mock implementations. Partly this is a problem with Java's type system (enthusiasts for exotic languages may pause here to remember their favourite), but I've gone the long way around in this chapter because I wanted to show, from first principles, where these techniques come from. I'll make it easier in the next chapter. </para> </section> <!-- Summary --> </chapter> Index: doc-book.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/doc-book.xml,v retrieving revision 1.7 retrieving revision 1.8 diff -u -r1.7 -r1.8 --- doc-book.xml 10 Aug 2002 12:22:59 -0000 1.7 +++ doc-book.xml 10 Aug 2002 18:11:00 -0000 1.8 @@ -8,6 +8,7 @@ <!ENTITY chp_servlets_1 SYSTEM "file://@docpath@/servlets_1.xml"> <!ENTITY chp_servlets_2 SYSTEM "file://@docpath@/servlets_2.xml"> + <!ENTITY chp_servlets_3 SYSTEM "file://@docpath@/servlets_3.xml"> <!ENTITY chp_jdbc_testfirst SYSTEM "file://@docpath@/jdbc_testfirst.xml"> @@ -45,6 +46,7 @@ &chp_servlets_1; &chp_servlets_2; + &chp_servlets_3; &chp_jdbc_testfirst; </part> |