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