From: Steve F. <sm...@us...> - 2002-08-11 14:09:32
|
Update of /cvsroot/mockobjects/no-stone-unturned/doc/xdocs In directory usw-pr-cvs1:/tmp/cvs-serv30187/doc/xdocs Modified Files: how_mocks_happened.xml Log Message: more rework Index: how_mocks_happened.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/how_mocks_happened.xml,v retrieving revision 1.4 retrieving revision 1.5 diff -u -r1.4 -r1.5 --- how_mocks_happened.xml 10 Aug 2002 23:43:41 -0000 1.4 +++ how_mocks_happened.xml 11 Aug 2002 14:09:28 -0000 1.5 @@ -49,7 +49,7 @@ <para> This test tells us that the robot is in the same place after the test as it was before the test. That's an essential requirement, but it's not enough. We can't be sure that the robot hasn't - trundled all the way around the warehouse before returning; we need to know that the Robot hasn't + trundled all the way around the warehouse before returning; we need to know that the robot hasn't moved. How about if the robot were to store the route it takes each time we call <methodname>goto()</methodname>? We could retrieve the route and make sure it's valid. For example: </para> @@ -60,14 +60,14 @@ Robot robot = new Robot(); robot.setCurrentPosition(POSITION); - robot.goTo(POSITION); + robot.goto(POSITION); <emphasis>assertEquals("Should be empty", 0, robot.getRecentMoveRequests().size());</emphasis> assertEquals("Should be same", POSITION, robot.getPosition()); }</programlisting> <para> - So far we've specified that the Robot will not move if asked to go to the same place. We've + So far we've specified that the robot will end up in the same place if that's where we ask it to go. We've also specified that it will store the route from it's most recent move, and something about the programming interface that it should support. Our next test is to move one point on the grid: </para> @@ -87,10 +87,17 @@ new MoveRequest(1, MoveRequest.SOUTH), moves.get(1)); }</programlisting> + <sidebar> + <para> + Of course, you've noticed that we haven't said anything about the layout of the warehouse. We'll + assume for now that it's regular and hard-coded into the Robot. + </para> + </sidebar> + <para> Now we've also specified the simplest routing scheme, for moves to adjacent squares, and that we're describing each leg of the route with a <classname>MoveRequest</classname> object. We carry on and - pretty soon we're moving the Robot all over the building, for example: + pretty soon we're moving the robot all over the building, for example: </para> <programlisting> @@ -111,33 +118,32 @@ to keep the tests legible. For example, <methodname>makeExpectedLongWayMoves()</methodname> returns a list of the moves we expect the robot to make for this destination. </para> + </section> <!-- Simple Unit Tests --> - <sidebar> - <para> - Of course, you've noticed that we haven't said anything about the layout of the warehouse. We'll - assume for now that it's regular and hard-coded into the Robot. - </para> - </sidebar> + <section> + <title>What's wrong with this?</title> <para> - There are difficulties with this approach to testing. First, tests like this are effectively - small-scale integration tests, they set up pre-conditions and test post-conditions but they have no - access to the code while it is running. If this test failed, the error report would say something like: + Tests like this are effectively small-scale integration tests, they set up pre-conditions and + test post-conditions. There are advantages to this technique because test failures can reveal interesting + dependencies between classes and help to drive refactorings. The disadvantage is that the tests + has no access to the code while it is running. If our last test failed, the error report would + say something like: </para> <screen> There was 1 failure: 1) testMoveALongWay(test.nostone.RobotTest)junit.framework.AssertionFailedError: Should be same moves:<[10, South; 3, East; 7, South; &elipsis;> but was: <[10, South; 3, East; 7, South; &elipsis;> - at test.notstone.RobotTest.testMoveALongWay() + at test.nostone.RobotTest.testMoveALongWay() FAILURES!!!</screen> <para> We will have to step through the code to find the problem because the assertions have - been made <emphasis>after</emphasis> the call has finished. Even so, the robot is a relatively simple example. - Some of us tried to do this with financial mathematics and it's very painful when a calculation fails. - A test failure usually meant stepping carefully through the code with an open spreadsheet nearby to check - the values. + been made <emphasis>after</emphasis> the call has finished. It could be worse, the robot is a relatively + simple example. Some of us tried to do this with financial mathematics and it's very painful when a + calculation fails. A test failure usually meant stepping carefully through the code with an open spreadsheet + nearby to check the values. </para> <para> @@ -157,15 +163,15 @@ </para> <para> - 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 testfailures—that will fail faster, - like we know we're supposed to? What would happen if we really believed in the object design - maxim <quote>Tell, don't ask</quote>? + 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 + <quote>Tell, don't ask</quote>? </para> - </section> <!-- Simple Unit Tests --> + </section> <!-- what's wrong with this --> <section> - <title>Breaking the Robot apart</title> + <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 @@ -173,19 +179,19 @@ <emphasis>and</emphasis> it has to move along the route it has chosen. Those activities are separate, even if they might happen at the same time. If we break out these responsibilities into two objects, we can also separate the testing that goes with them. We can test that the route planning - object creates a suitable route and that the object that moves the Robot follows that route correctly. + object creates a suitable route and that the Robot moving object follows that route correctly. + </para> + <para> We'll start with the "Robot Moving Object", what would be a good name for such a component? How about <emphasis>Motor</emphasis>? If we leave the route planning in the <classname>Robot</classname> object, we can intercept the requests that the <classname>Robot</classname> makes to its - <classname>Motor</classname>, which means that we can see inside without holding on to the route data. - </para> - - <para> - We'll start by defining an interface for <classname>Motor</classname>. We know that - there must be some kind of request, which we'll call <methodname>move()</methodname>, that takes some - kind of move request as a parameter. We don't yet know what's in that request, so we'll define an empty - <classname>MoveRequest</classname> interface as a placeholder to get us through the compiler. In dynamic - libraries, such as Smalltalk and Python, we don't even need to do that. + <classname>Motor</classname>, which means that we would be able to see inside the Robot without holding + on to the route data. + First we define an interface for <classname>Motor</classname>. We know that there must be some kind of + request, which we'll call <methodname>move()</methodname>, that takes some kind of move request as a parameter. + We don't yet know what's in that request, so we'll define an empty <classname>MoveRequest</classname> + interface as a placeholder to get us through the compiler. (Of course, in dynamic languages, such as Smalltalk + and Python, we don't even need to do that.) </para> <programlisting> @@ -193,49 +199,68 @@ void move(MoveRequest request); }</programlisting> - <comment>From here!</comment> - <para> - Now I need to initialise a Robot with a Motor when I create it. - Because I want to intercept the interaction between the Robot and its Motor - I cannot let the Robot instantiate its own Motor; there would be no way to - then intercept the Robot's movement requests. That leads me to pass a - Motor instance to the Robot's constructor. + Now that we've separated out the motor from the robot, where does the <classname>Robot</classname> object get its + instance of the <classname>Motor</classname> class? We don't yet know what a real <classname>Motor</classname> + looks like, so we can't just create a default instance. We could, however, pass in an temporary implementation + during the test, so we'll add a <classname>Motor</classname> to the <classname>Robot</classname>'s constructor. </para> <para> - I can now write my tests to create a Robot with an implementation - of the <classname>Motor</classname> interface, that watches what's - happening in the Robot, and complains as soon as something goes wrong. - In fact, I will do this right now, before I start thinking about writing a - real implementation of the Motor interface, so that I know my Robot - implementation still works despite the extensive refactorings I have - performed. The first test is now: + Now we have to decide what the test implementation will do. In this test, we want to be sure that the + <classname>Robot</classname> stays in place, so the test <classname>Motor</classname> should simply + fail if it receives any requests to move. We can write this test now, before we know anything else about + the system, which means that we have locked down a little piece of the specification. We can be sure that, + however complex our routing code gets, the <classname>Robot</classname> will not move if asked to go to + its current position. The new version of the test is: </para> - <programlisting>public void testGotoSamePlace() { - final Position POSITION = new Position(0, 0); - robot.setCurrentPosition(POSITION); + <programlisting> +public void testGotoSamePlace() { + final Position POSITION = new Position(1, 1); - Motor mockMotor = new Motor() { + <emphasis>Motor mockMotor = new Motor() { public void move(MoveRequest request) { - fail("There should be no moves in this test"); + fail("Should be no moves"); } }; - robot.setMotor(mockMotor); + Robot robot = new Robot(mockMotor);</emphasis> + robot.setCurrentPosition(POSITION); robot.goTo(POSITION); assertEquals("Should be same", POSITION, robot.getPosition()); }</programlisting> -<para>In this test, if there is a bug in the Robot code and the Motor -gets requested to move, the mock implementation of -<function>move()</function> -will fail immediately and stop the test; I no longer need to ask the Robot -where it's been.</para> + <para> + Now if there's a bug in the robot routing code that asks the motor to move, the test will fail at the + point that the request is made. The error report might look like: + </para> + + <screen> +There was 1 failure: +1) testGotoSamePlace(test.nostone.RobotTest)junit.framework.AssertionFailedError: + Should be no moves + at test.nostone.RobotTest.testGotoSamePlace(test.nostone.RobotTest); + at tdd.nostone.RobotTest$Motor.move() + at tdd.nostone.Robot.requestNextMove() + at tdd.nostone.Robot.chooseBestPath() + at tdd.nostone.Robot.arriveAtJunction() + &elipsis; +FAILURES!!!</screen> + + <para> + which gives a much clearer view of where the error became visible. If finding the problem turns out to be + harder, we can trap the <classname>junit.framework.AssertionFailedError</classname> in the development + tool to bring up the debugger. Then we can explore the program state at the time of the failure, without + having to step through from the beginning of the test. + </para> + + + <!-- TODO --> + -<para>Now I know that my Robot class works I can write a real implementation +<para>Now I know that my Robot class works I can write a real implementation of the Motor interface:</para> <programlisting> |