From: Steve F. <sm...@us...> - 2002-10-06 14:18:42
|
Update of /cvsroot/mockobjects/no-stone-unturned/doc/xdocs In directory usw-pr-cvs1:/tmp/cvs-serv31067/doc/xdocs Modified Files: how_mocks_happened.xml _template.xml glossary.xml a_longer_example.xml patterns.xml Log Message: further work on how_mocks_happened Index: how_mocks_happened.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/how_mocks_happened.xml,v retrieving revision 1.17 retrieving revision 1.18 diff -u -r1.17 -r1.18 --- how_mocks_happened.xml 5 Oct 2002 23:35:56 -0000 1.17 +++ how_mocks_happened.xml 6 Oct 2002 14:18:37 -0000 1.18 @@ -5,20 +5,22 @@ <title>Introduction</title> <para> - Mock Objects is a development technique that lets you unit test classes that you didn't think - you could <emphasis>and</emphasis> helps you write better code while doing so. This chapter - works through a simple example to show a compressed history of how Mock Objects was discovered by - refactoring from conventional unit tests and why they're useful. The chapter is unusual in that it's - focussed on the tests, it ignores the production code almost completely. - I'll be covering the whole cycle in later chapters but, for now, I want to concentrate - on the thought processes behind our approach to &TDD;. + Mock Objects is a development technique based on experience. It was discovered and refined amongst + the Extreme Programming community in London as we taught ourselves &TDD;, because we had people who + would ask the right pointed question and people who could make ideas work. We've stuck with + Mock Objects more than most teams because of the benefits we see, such as better code structure, + understandable unit tests, and testing against complex infrastrcture. I'll justify all of these + claims later in the book but, for now, I want to describe how we got there by refactoring + from conventional unit tests and why. This makes for an unusual chapter that's all tests and no + production code. </para> <para> - The concept of Mock Objects is based on two essential techniques: isolate the code under test by + The concept of Mock Objects is based on two techniques: isolate the code under test by emulating everything it interacts with and, put the test assertions <emphasis>inside</emphasis> those stub implementations so that you can validate the interactions between objects as they occur. - This sounds simple, but it has some powerful effects on how the code comes out. + This sounds simple — it <emphasis>is</emphasis> simple — but it has some powerful effects + on how we write software. </para> </section> <!-- Introduction --> @@ -58,7 +60,7 @@ </para> <programlisting> -public void testGotoSamePlace() { +public void testDoNotMove() { final Position POSITION = new Position(0, 0); Robot robot = new Robot(); @@ -89,17 +91,12 @@ assertEquals("Should be same move", new MoveRequest(1, MoveRequest.SOUTH), moves.get(1)); }</programlisting> - <note> - <para> - You may have noticed that we haven't said anything about the layout of the warehouse. We'll - assume for now that the Robot knows about it by some other mechanism. - </para> - </note> - <para> This test specifies two things: a simple routing scheme for moves to adjacent squares, and that we're - describing each step of the route with a <classname>MoveRequest</classname> object. We carry on and - pretty soon we have tests for moving the robot all over the building, for example: + describing each step of the route with a <classname>MoveRequest</classname> object. You may have noticed + that we haven't said anything about the layout of the warehouse, we'll assume for now that the Robot + knows about it by some other mechanism. We carry on and pretty soon we have tests for moving the robot + all over the building, for example: </para> <programlisting> @@ -169,7 +166,7 @@ Is there a better way? Can we find a technique that's less intrusive, that doesn't require the robot to hang on to unnecessary values? Can we have more helpful test failures that will fail faster, like we know we're supposed to? What would happen if we really believed in the object maxim - <glossterm linkend="telldontask"><quote>Tell, don't ask</quote></glossterm>? That is, what if we make a point + <glossterm linkend="telldontask"><quote>Tell, don't ask</quote></glossterm>? What if we make a point of avoiding getters? <footnote><para> John Nolan is credited with asking this question at the right time at a software architecture @@ -182,7 +179,8 @@ <title>Breaking apart the Robot</title> <para> - Let's stop and think for a moment. Our robot is actually doing two things when we ask it to move + Let's stop and think for a moment. Which object within a robot can tell that the correct route has been + generated? Our robot is actually doing two things when we ask it to move through the warehouse; it has to choose a route from its current position to its destination, <emphasis>and</emphasis> it has to move along the route it has chosen. Those activities are distinct, even if they might happen at the same time. If we break out these responsibilities into two @@ -227,7 +225,7 @@ </para> <programlisting> -public void testGotoSamePlace() { +public void testDoNotMove() { final Position POSITION = new Position(1, 1); <emphasis>Motor mockMotor = new Motor() { @@ -262,9 +260,9 @@ <screen> There was 1 failure: -1) testGotoSamePlace(test.nostone.RobotTest)junit.framework.AssertionFailedError: +1) testDoNotMove(test.nostone.RobotTest)junit.framework.AssertionFailedError: Should be no moves - at test.nostone.RobotTest.testGotoSamePlace(test.nostone.RobotTest); + at test.nostone.RobotTest.testDoNotMove(test.nostone.RobotTest); at tdd.nostone.RobotTest$Motor.move() at tdd.nostone.Robot.requestNextMove() at tdd.nostone.Robot.chooseBestPath() @@ -545,7 +543,7 @@ }); } - public void testGotoSamePlace() { + public void testDoNotMove() { final Position POSITION = new Position(1, 1); moveAndVerifyRobot(POSITION, POSITION); } @@ -755,18 +753,41 @@ holds together the objects that do the real work. This is how the process often works: we start with a top-level object, fill in some implementation, and hollow it out again as we understand more about what's inside. We then do the same to the new objects we've created, and so on. We end up with a collection of - classes, each focussed on a particular task, that have well-defined (and tested!) interfaces to their collaborating - objects. + classes, each focussed on a particular task, that have well-defined (and tested!) interfaces + to their collaborating objects. </para> + <para> + One more design issue is our unusually heavy use of the <quote>Tell, don't ask</quote> pattern. + We prefer to pass behaviour into an object, rather than pulling values out, which is why we wrote + a <quote>route following object</quote> rather than returning a collection of + <classname>MoveRequest</classname>. + My experience is that the common use of getters, particularly for collections, makes a class and + the code that uses it that little bit harder to change. Across a whole code base, the increased + <quote>refactoring drag</quote> can be significant. If it's not clear why getting collections can slow + things down, think of all the times you write iterator loops such as: + </para> + <programlisting> + Iterator moveRequests = robot.getMoveRequests(); + while (moveRequests.hasNext()) { + MoveRequest moveRequest = (MoveRequest)moveRequests.next(); + <lineannotation>// process moveRequest</lineannotation> + }</programlisting> + <para> + If we change to passing a <classname>Motor</classname> to a <classname>Robot</classname> which, in turn, + passes each step on the route to the <classname>Motor</classname>, then we've refactored the loop navigation; + it's done exactly once in the <classname>Robot</classname>. We've also made test failures more + accurate. Some of us think that this style is more expressive because it leaves the decision about + route management where it belongs, with the <classname>Robot</classname>, rather than the caller. + </para> + <sidebar> <title>Top-Down Decomposition</title> <para> Some of you may recognise the similarities between &TDD; and the ancient discipline of - <glossterm linkend="topdowndecomposition"><quote>Top-Down Decomposition</quote></glossterm>, also known as - <quote>Structured Programming</quote>. The idea is to start from the top-most level of the program, stub out - the bits that have not yet been implemented, and work your way down; repeat until you have a complete - application. In the days when many programmers found procedures and nested scope exotic, - Top-Down Decomposition was a useful technique for avoiding + <quote>Top-Down Decomposition</quote>, also known as <quote>Structured Programming</quote>. The idea + is to start from the top-most level of the program, stub out the bits that have not yet been implemented, + and work your way down; repeat until you have a complete application. In the days when many programmers + found procedures and nested scope exotic, Top-Down Decomposition was a useful technique for avoiding <glossterm linkend="sdd">Spaghetti-Driven Development</glossterm>. </para> <para> @@ -775,78 +796,33 @@ a couple of critical failings: it turned out to be hard to change early design decisions because, of course, they're embedded in the top-level structure, and it's not good at encouraging reuse between lower-level components. It looks like &TDD; avoids these problems because - object-orientation makes it easier to share components throughout the application and - the emphasis on refactoring means we can remove duplication as the codebase grows. + the combination of object-orientation and refactoring makes it easier to make radical changes to + the code, and to remove duplication by sharing components. </para> </sidebar> <!-- Top-Down Decomposition --> - <para> - One more design issue is our unusually heavy use of the <quote>Tell, don't ask</quote> pattern. - We prefer to pass behaviour into an object, rather than pulling values out, which is why we wrote - a <quote>route following object</quote> rather than returning a collection of - <classname>MoveRequest</classname>. - My experience is that the common use of getters, particularly for collections, makes a class and - the code that uses it that little bit harder to change. Across a whole code base, the increased - <quote>refactoring drag</quote> can be significant. If it's not clear why getting collections can slow - things down, think of all the times you write iterator loops such as: - </para> - <programlisting> -Iterator moveRequests = robot.getMoveRequests(); -while (moveRequests.hasNext()) { - MoveRequest moveRequest = (MoveRequest)moveRequests.next(); - <lineannotation>// process moveRequest</lineannotation> -}</programlisting> - <para> - If we change to passing a <classname>Motor</classname> to a <classname>Robot</classname> which, in turn, - passes each step on the route to the <classname>Motor</classname>, then we've refactored the loop navigation; - it's done exactly once in the <classname>Robot</classname>. We've also made test failures more - accurate. Some of us think that this style is more expressive because it leaves the decision about - route management where it belongs, with the <classname>Robot</classname>, rather than the caller. - </para> </section> <!-- What about design? --> <section> <title>What does this mean?</title> <para> - Mock Objects is a technique + At one level, Mock Objects are just stubs with assertions, but using them consistently within &TDD; + changes the way I write code (for the better, I think). It gives me a framework for identifying + objects and describing how they talk to each other; deciding what to verify forces me to clarify the + relationships between an object and its collaborators. Mock Objects makes the tests easier to understand + because each test exercises just one feature in an object. In turn, that makes refactoring easier + because there are fewer test side-effects when I change the code. The overhead of writing Mock Objects + may look high from this example but it's not like that in practice. Modern development environments will do + a lot of the work and a project, as it grows, will reach a critical mass of test infrastructure so that + new tests don't always require new Mock Objects — the benefits of refactoring apply as much to the + tests as to the production code. </para> -<para>Tests based on Mock Objects usually conform to a pattern: setup any -state, set expectations for the test, run the target code, and verify -that your expectations have been met. This style makes tests easy to -work with because they look similar and because all the interactions -with an object are local to a test fixture; I have found myself -contributing usefully to someone else's code base after only a few -minutes with the tests. More importantly, however, I constantly find -that the process of deciding what to verify in a test drives me to -clarify the relationships between an object and its collaborators. -The flex points I add to my code to provide support for testing turn -out to be the flex points I need as the use of the code evolves. -</para> -</section> <!-- What does this mean? --> - - <section> - <title>Conclusions</title> - - <para> - This simple example shows how refactoring tests with some design principles in mind led to the discovery of - an unusually fruitful development technique. Using Mock Objects in practice is slightly different. First, - the process of writing a test usually involves defining which mock objects are involved, rather than - extracting them afterwards. Second, in Java at least, there are the beginnings of a Mock Object library - for common classes and APIs. Third, there are now several tools and libraries to help with the construction - of Mock Objects. In particular, the <ulink url="http://www.mockobjects.com">www.mockobjects.com</ulink> site - includes a library of expectation objects. - </para> - <para> - The rest of this book will show you how to use Mock Objects and Test Driven Design in your development process. - We will work through some real-world examples to show how Mock Objects can be used to test Java APIs, drive - refactoring and eliminate dependencies on external components. And along the way we will annotate our examples - with tips and warnings to help you improve your technique and avoid pitfalls. - </para> <para> - Mock Objects and Test Driven Design have changed the way we develop code, and changed it noticeably for the - better. We hope that you can benefit from these techniques as well and that this book helps you to do so. + The critical insight of &TDD; is that tests are a design tool, not just for testing; the flex points I + add to make my code testable turn out to be the flex points I need as the use of the system evolves. + The critical insight of Mock Objects is that stubs are a design tool as well. </para> - </section> <!-- Conclusions --> + </section> <!-- What does this mean? --> </chapter> Index: _template.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/_template.xml,v retrieving revision 1.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- _template.xml 12 Aug 2002 23:08:53 -0000 1.4 +++ _template.xml 6 Oct 2002 14:18:37 -0000 1.5 @@ -12,15 +12,21 @@ ]> <article> + <articleinfo> + <author> + <firstname>Steve</firstname> <surname>Freeman</surname> + <email>st...@m3...</email> + </author> + <copyright> + <year>@year@</year> + <holder>Steve Freeman</holder> + </copyright> + </articleinfo> + &@fragment.name@; <!-- need these to avoid dangling references in the fragment --> - <section> - <title>Patterns</title> - &patterns; - </section> - + &patterns; &glossary; -</article> - +</article> \ No newline at end of file Index: glossary.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/glossary.xml,v retrieving revision 1.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- glossary.xml 20 Aug 2002 16:56:57 -0000 1.4 +++ glossary.xml 6 Oct 2002 14:18:37 -0000 1.5 @@ -44,14 +44,6 @@ </glossdiv> <!-- S --> <glossdiv> - <title>T</title> - - <glossentry id="topdowndecomposition"> - <glossterm>Top-Down Decomposition</glossterm> - </glossentry> - </glossdiv> <!-- T --> - - <glossdiv> <title>W</title> <glossentry id="wiki"> Index: a_longer_example.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/a_longer_example.xml,v retrieving revision 1.5 retrieving revision 1.6 diff -u -r1.5 -r1.6 --- a_longer_example.xml 8 Sep 2002 15:18:57 -0000 1.5 +++ a_longer_example.xml 6 Oct 2002 14:18:37 -0000 1.6 @@ -332,7 +332,7 @@ </section> <!-- We write our first implementation of the servlet --> <section> - <title>We accept a name and return a result from a hard-coded collection.</title> + <title>Handle no result and missing request name.</title> <para> Decided to return a single value for a known name. @@ -514,8 +514,111 @@ A useful clue is that we're not using "surname", the value of the name parameter. So, the test is driving us to add the book logic to the servlet. </para> - </section> <!-- accept a name and return a result from a hard-coded collection --> + <para> + First thing, rename testNoEntries to testMissingName and change expected return string. + </para> + + <programlisting> + assertEquals("Should be error message", "No name", page.toString().trim()); + </programlisting> + + <para> + Let's finish refactoring test case. Both tests use near identical MockRequest, but change on + returned parameter. Use a String instance variable to pass found name to MockRequest. + </para> + + <programlisting> + private String requestParameter; + private final ExpectationValue requestParameterName = new ExpectationValue("Request parameter name"); + private AddressBookServlet servlet = new AddressBookServlet(); + private HttpServletRequest mockRequest = new NullHttpServletRequest() { + public String getMethod() { + return "GET"; + } + + public String getProtocol() { + return "HTTP/1.1"; + } + + public String getParameter(String parameterName) { + requestParameterName.setActual(parameterName); + return requestParameter; + } + }; + + public void testMissingName() throws ServletException, IOException { + requestParameterName.setExpected("name"); + + servlet.service(mockRequest, mockResponse); + + requestParameterName.verify(); + assertEquals("Should be error message", "No name", page.toString().trim()); + } + + public void testFindOneAddress() throws ServletException, IOException { + requestParameterName.setExpected("name"); + + requestParameter = "surname"; + servlet.service(mockRequest, mockResponse); + + requestParameterName.verify(); + assertEquals("Should be expected address", "surname@domain", page.toString().trim()); + }</programlisting> + + <comment> + Things have been a bit misguided so far. We should have been testing for the name parameter from + the start as that's in the requirements. Need to rework this section to get to this point. Now + </comment> + </section> <!-- Handle no result and missing request name --> + + <section> + <title>Now we return a found address for a hard-coded name</title> + + <para> + Add a test for a found address. + </para> + + <programlisting> + public void testFindOneAddress() throws ServletException, IOException { + requestParameterName.setExpected("name"); + + requestParameter = "somebody"; + servlet.service(mockRequest, mockResponse); + + requestParameterName.verify(); + assertEquals("Should be expected address", "somebody@domain", page.toString().trim()); + }</programlisting> + + + <para> + Now we have two tests based on the requested name that return different results. How are we going to + tell the servlet? First, just hard-code it. We'll pull the implementation into a helper method to + make it more readable. (Yes, it's disgusting). + </para> + + <programlisting> + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + String name = request.getParameter("name"); + + response.setContentType("text/plain"); + response.getWriter().println( + lookupName(name)); + } + + private String lookupName(String name) { + if (name == null) { + return "No name"; + } else if (name.equals("somebody")) { + return "somebody@domain"; + } else { + return "No address found"; + } + }</programlisting> + + + </section> <!-- Now we return a found address for a hard-coded name --> <!-- TODO --> <section> Index: patterns.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/patterns.xml,v retrieving revision 1.3 retrieving revision 1.4 diff -u -r1.3 -r1.4 --- patterns.xml 18 Aug 2002 22:44:39 -0000 1.3 +++ patterns.xml 6 Oct 2002 14:18:37 -0000 1.4 @@ -1,4 +1,5 @@ <glossary status="draft"> + <title>Patterns</title> <glossdiv> <title>A</title> |