From: Steve F. <sm...@us...> - 2002-10-05 23:35:58
|
Update of /cvsroot/mockobjects/no-stone-unturned/doc/xdocs In directory usw-pr-cvs1:/tmp/cvs-serv9684/doc/xdocs Modified Files: how_mocks_happened.xml Log Message: reworking Index: how_mocks_happened.xml =================================================================== RCS file: /cvsroot/mockobjects/no-stone-unturned/doc/xdocs/how_mocks_happened.xml,v retrieving revision 1.16 retrieving revision 1.17 diff -u -r1.16 -r1.17 --- how_mocks_happened.xml 2 Sep 2002 20:13:29 -0000 1.16 +++ how_mocks_happened.xml 5 Oct 2002 23:35:56 -0000 1.17 @@ -8,22 +8,17 @@ 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. - </para> - - <para> - Mock Objects is based on two key concepts: replace everything except the code under test with - mock implementations that emulate the rest of the environement, and put the test assertions - <emphasis>inside</emphasis> those mock implementations so that you can validate the interactions - between the objects in your test case as they occur. + 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;. </para> <para> - This 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 a particular approach to &TDD;. This chapter is not about - how to develop routing software for a robot, but how to structure your code so that you can tell if such a - module works. + The concept of Mock Objects is based on two essential 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. </para> </section> <!-- Introduction --> @@ -102,7 +97,7 @@ </note> <para> - This test specifies a simple routing scheme, for moves to adjacent squares, and that we're + 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: </para> @@ -148,7 +143,7 @@ <para> We will have to re-run the code to find the problem because the assertions have 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 was very painful when a + simple example. I was on a team that tried to do this with financial mathematics and it was very painful when a calculation failed. A test failure usually meant carefully stepping through the code with an open spreadsheet nearby to check the intermediate values. </para> @@ -158,8 +153,9 @@ to hold all the <classname>MoveRequest</classname>s since the last <methodname>goTo()</methodname>. We don't have any other immediate need for <methodname>getRecentMovesRequests()</methodname> and, what's worse, we've made an implicit promise to other people who work with the <classname>Robot</classname> - code that we will store routes, thus shrinking (ever so slightly) our room to manouver. We can mark the method as - test infrastructure, but this sort of notation is clumsy and is often ignored in the heat of development. + code that we will store routes, thus shrinking (ever so slightly) our room to manouver. We could mark the + method as test infrastructure, but this sort of notation is clumsy and is often ignored in the heat + of development. </para> <para> @@ -247,14 +243,13 @@ assertEquals("Should be same", POSITION, robot.getCurrentPosition()); }</programlisting> - <note> - <para> - I'm being inconsistent again by keeping <function>getCurrentPosition()</function>. In my defence, - it seems more reasonable to me that a robot should know its current position than that it should - keep a list of its recent movements. One day I'll replace this with a - <classname>CurrentPositionListener</classname>, or some such technique. Really I will. - </para> - </note> + <para>Now we have a little object cluster that looks like this:</para> + + <informalfigure> + <mediaobject><imageobject> + <imagedata fileref="figures/how_mocks_happened_1.gif" align="center"/> + </imageobject></mediaobject> + </informalfigure> <para> This version of the test still describes the same core specification as before (the @@ -280,10 +275,22 @@ <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 &junit; <classname>AssertionFailedError</classname> exception in the development - tool to bring up the debugger. Then we can explore the program state at the time of the failure, without + 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. This technique doesn't work all the time but, more often than not, it takes you to straight to the source of the problem. </para> + + <note> + <para> + I'm being inconsistent again by keeping + <function>getCurrentPosition()</function>. In my defence, + it seems more reasonable to me that a robot should know its current position than that it should + keep a list of its recent movements. One day I'll replace this with a + + <classname>CurrentPositionListener</classname>, or some such technique. Really I will. + + </para> + </note> </section> <!-- Breaking apart the Robot --> <section> @@ -317,29 +324,29 @@ <para> The first assertion will fail if the <classname>Robot</classname> sends the wrong - <classname>MoveRequest</classname>. The second assertion will fail if the - <classname>Robot</classname> calls <function>move()</function> more than once. This test won't - fail if the <classname>Robot</classname> doesn't call <function>move()</function> at all, as it won't - try either assertion. We can only tell whether this has happened after - <function>goTo()</function> has finished, so we need to record how many times <function>move()</function> - was called. Unfortunately, we can't just push <varname>moveCount</varname> up from the anonymous - <classname>Motor</classname> class to the test method because a quirk of Java requires such variables to - be declared <token>final</token> and we can't change the value of a <type>final int</type>. I can think of - two alternatives that would solve our immediate problem. + <classname>MoveRequest</classname> and the second assertion will fail if the + <classname>Robot</classname> calls <function>move()</function> more than once. This test, however, won't + fail if the <classname>Robot</classname> forgets to call <function>move()</function> altogether because + then it won't try any of the assertions. We can only find missing calls after <function>goTo()</function> + has finished, so we need to record how many times <function>move()</function> is called. Unfortunately, we + can't just push <varname>moveCount</varname> up from the anonymous <classname>Motor</classname> class to the + test method because Java requires such variables to be declared <token>final</token> and we can't + change the value of a <type>final int</type>. I can think of two alternatives that would solve + our immediate problem. </para> <para> First, we could write a named implementation of <classname>Motor</classname> so that the test can see the <varname>moveCount</varname> field. </para> <programlisting> -<emphasis>static class AbstractMotorStub implements Motor { +<emphasis>static class CountingMotorStub implements Motor { int moveCount = 0; }</emphasis> public void testMoveOneStep() { final Position DESTINATION = new Position(1, 0); - AbstractMotorStub mockMotor = new AbstractMotorStub() { + CountingMotorStub mockMotor = new CountingMotorStub() { public void move(MoveRequest request) { assertEquals("Should be move", new MoveRequest(1, MoveRequest.SOUTH), request); moveCount++; @@ -385,7 +392,8 @@ <para> Personally, I prefer the first of these two options because I find it easier to see the boundaries - between the stub and the test classes. That said, there's not a huge difference, there are plenty of + between the stub and the test classes. It also sets me up better for pulling out a mock implementation that + I can share between tests. That said, there's not a huge difference, there are plenty of people making effective use of Self Shunt, and it's more important that your team is consistent about its approach. <tip> @@ -421,7 +429,7 @@ <para> We can use the <varname>expectedMoveRequests</varname> in the mock motor. Every time an actual - move arrives from the robot, we pull out the next value from the list of expected moves and compare the + move arrives from the robot, we pop the next value from the list of expected moves and compare the two. If there's a mismatch then our routing software has gone wrong and the test fails. </para> @@ -491,7 +499,7 @@ original <quote>playback</quote> version that recorded the most recent movements. The answer is that, by moving the collection from the production code into the test infrastructure, we've made our design testable without adding functionality that the system doesn't need. We've kept our options a little bit more open and, - as a side benefit, we have much more useful failure messages. + as a side benefit, we have more useful failure messages. </para> </section> <!-- Testing more than one step --> @@ -602,13 +610,12 @@ <tip> <title>Go to Lourdes</title> <para> - Throw away your software crutches occasionally to test your assumptions just one more time; - this is one of the movitivations for the XP rule - <glossterm linkend="dtsttcpw"><quote>Do The Simplest Thing That Could Possibly Work</quote></glossterm>. - I'm always surprised how much better I can do something the next time around after I have more - experience. For example, I've found that writing the examples for this book a real eye-opener - because I've had to work out some of my practices from first principals again. - (Lourdes is a shrine in France famous for healing the lame, the walls are covered in discarded crutches.) + I've found writing the examples for this book a real eye-opener because I've had to work out + some of my practices from first principals again. Sometimes it's a good idea to throw away your + software crutches occasionally to test your assumptions just one more time, which is one of the + motivations for the XP rule <glossterm linkend="dtsttcpw"><quote>Do The Simplest Thing That + Could Possibly Work</quote></glossterm>. (Lourdes is a shrine in France famous for healing the + lame, the walls are covered in discarded crutches.) </para> </tip> </para> @@ -645,7 +652,7 @@ }</programlisting> <para> - Starting a new class costs a little extra, but it does bring some benefits. + Converting to a named class costs a little extra, but it does bring some benefits. First, it makes the tests a bit more legible because it moves the behaviour (managing lists of move requests) to the object that should know about it (the <classname>Motor</classname>). If we rework one of our tests, it comes out as: @@ -692,10 +699,10 @@ <tip> <title>Steady promotion</title> <para> - Often, I evolve a mock class by starting with an anonymous implementation in the test class, + Nowadays, I often evolve a mock class by starting with an anonymous implementation in the test class and then extracting a separate mock class if the code becomes anything more than trivial. The mock - class may start as an inner class of the test case (because my development environment makes - that refactoring easy), which I'll promote to package level if I need it elsewhere. + class may start as an inner class of the test case, because my development environment makes + that refactoring easy, which I'll promote to package level if I need it elsewhere. </para> </tip> @@ -717,12 +724,11 @@ decided that we needed a <quote>route following object</quote>, a <classname>Motor</classname>, so that we could tell whether the route has been properly constructed; we invented a construct in the software to represent something - in the physical world. This sounds to me like Object-Oriented Design (or at least some of it), - and we have found a way to drive that decision from an immediate requirement in a test. - A motor is straightforward, in later examples I'll show how we can be more sophisticated and - invent objects to represent computatational concepts such as callbacks and listeners. - </para> - <para> + in the physical world. This sounds to me like Design, and we have driven that + decision from an immediate requirement in a test. + A motor is a straightforward concept, in later examples I'll show how we can be more + sophisticated and invent objects to represent computatational concepts such as callbacks + and listeners. The next decision was how to connect the motor object to the robot object, so that it can move. We could have passed the <classname>Motor</classname> through with the <function>goTo()</function> method, but instead we decided to make the <classname>Motor</classname> @@ -731,25 +737,25 @@ </para> <para> We're designing each unit of code from the outside-in, from how the caller sees it. When we - program, writing tests with Mock Objects makes us think about the objects it - depends on, its environment. Sometimes those objects don't yet exist, which means that we've + program, writing tests with Mock Objects makes us think about the other objects that the target code + depends on, its environment. Sometimes those objects don't exist yet, which means that we've found a conceptual <quote>fault line</quote> in the system that we can turn into - a new object interface. In our tests, the expectations describe what the objects should say to each other, + a new interface. In our tests, the expectations describe what the objects should say to each other, their <emphasis>protocol</emphasis>, without having to figure out how the new objects actually work. We fake them up for now and move on to implementing them when we're ready. Applied consistently, this pushes us towards a codebase made up of components with clear interfaces which (at least in my experience) makes good things like reusability happen almost as a byproduct. What makes &TDD; so powerful is that we can do this incrementally, evolving the code from the requirements in hand rather than - trying to place bets on the what we think we're going to need. + having to place bets on the what we think we will need. </para> <para> - Speculating wildly, the next requirements might push us to extract a <classname>Navigator</classname> - object that works out the route, so that the <classname>Robot</classname> becomes just a frame that + Speculating wildly, the next requirements in this example might push us to extract a <classname>Navigator</classname> + object that works out the route, so that eventually the <classname>Robot</classname> becomes just a frame that 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 as we understand more about what's + 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 interfaces to their collaborating + classes, each focussed on a particular task, that have well-defined (and tested!) interfaces to their collaborating objects. </para> <sidebar> @@ -772,24 +778,19 @@ 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. </para> - </sidebar> + </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>. - Our experience is that the common use of getters, particularly for collections, makes a class and + 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. - </para> - - <note> - <para> - 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> + <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(); @@ -797,74 +798,19 @@ }</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. - At the very least, we've made the tests more accurate. If we need to iterate over this collection in more - than one place, then we've also removed some duplication. Some of us think that this style is also more - expressive because it leaves decisions about route management with the <classname>Robot</classname>, rather - than the caller. + 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> - </note> - - </section> <!-- What about design? --> - <!-- TODO --> <comment>Under development</comment> - <para> - Design by composition, top-down with polymorphism, do the protocols first, shows the fault lines but - it's more than divide and conquer. - </para> - - <section> <title>What does this mean?</title> -<para>My code moved in this direction because I was committed to unit -testing but didn't want to record and expose unnecessary details about the -state of my Robot -(the <function>getRecentMoveRequests()</function> method). -As a result, I have better unit tests and a cleaner interface to the Robot -class. But even better than that, by following the needs of my tests I have -actually ended up improving the object model of the Robot by separating the -Robot from its Motor and defining the an abstract interface between the two. -I now have the flexibility to subtitute a different Motor implementation, -perhaps one that accelerates. Similarly, if I want to track the total -distance a Robot travels, I can do so without changing the implementation -of either the robot or its motor:</para> - -<programlisting> -/** A decorator that accumulates distances, then passes the request - * on to a real Motor. - */ -public class MotorTracker implements Motor { - private Motor realMotor; - private long totalDistance = 0; - - public MotorTracker(Motor aRealMotor) { - realMotor = aRealMotor; - } - public void move(MoveRequest request) { - totalDistance += request.getDistance(); - realMotor.move(request); - } - public long getTotalDistance() { - return totalDistance; - } -} - -// When constructing the Robot, wrap the implementation of a -// Motor that does the work in a MotorTracker. -OneSpeedMotor realMotor = new OneSpeedMotor(); -MotorTracker motorTracker = new MotorTracker(realMotor); -Robot = new Robot(motorTracker); -// do some travelling here &elipsis; -println("Total distance was: " + motorTracker.getTotalDistance()); -</programlisting> - -<para>Neither changing the Motor implementation nor adding tracking -functionality would have been so easy if I had stuck with a testing -strategy based on stashing the intermediate route. The next step might -be to introduce a <classname>Navigator</classname> object to work out -the route, and the Robot would link the two together.</para> + <para> + Mock Objects is a technique + </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 |