From: Steve F. <sm...@us...> - 2002-08-09 23:49:42
|
Update of /cvsroot/mockobjects/no-stone-unturned/doc/xdocs In directory usw-pr-cvs1:/tmp/cvs-serv6874/doc/xdocs Added Files: how_mocks_happened.xml Log Message: Renamed from Introduction --- NEW FILE: how_mocks_happened.xml --- <chapter> <title>How Mock Objects Happened</title> <section> <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 whilst doing so. This chapter uses 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. </para> </section> <!-- Introduction --> <section> <title>Simple Unit Tests</title> <para> Let's dive straight into a worked example. We're writing software to direct robots through a warehouse. Given a starting point on a grid, it will find a route through the stacks to a specified destination. How can we test this? The standard approach is to write some unit tests that create a robot, call some methods on it, and then check that the resulting state is what we expect. Our tests will have to tell us, for example, that the robot arrives where it was directed to go and does not hit anything on the way. A easy test to start with is to check that a robot does not move if it is told to go to its current location: </para> <programlisting> public class TestRobot { [...] public void setUp() { robot = new Robot(); } public void testGotoSamePlace() { final Position POSITION = new Position(1, 1); robot.setCurrentPosition(POSITION); robot.goTo(POSITION); assertEquals("Should be same", POSITION, robot.getPosition()); } }</programlisting> <para> This tells us that the robot thinks that it has arrived at the right place, but it doesn't tell us anything about how it got there; it might have trundled all the way round the warehouse before returning to its original location. We would like to know that the Robot hasn't moved. One option would be to store and retrieve the route it took after each <methodname>goto()</methodname>. For example: </para> <programlisting> public void testGotoSamePlace() { final Position POSITION = new Position(0, 0); robot.setCurrentPosition(POSITION); robot.goTo(POSITION); assertEquals("Should be empty", 0, robot.getRecentMoveRequests().size()); assertEquals("Should be same", POSITION, robot.getPosition()); }</programlisting> <para>The test might be to move one point on the grid:</para> <programlisting> public void testMoveOnePoint() { final Position DESTINATION = new Position(1, 0); robot.setCurrentPosition(new Position(0, 0)); robot.goto(DESTINATION); assertEquals("Should be destination", DESTINATION, robot.getPosition()); List moves = robot.getRecentMoveRequests(); assertEquals("Should be one move", 1, moves.size()); assertEquals("Should be same move", new MoveRequest(1, MoveRequest.SOUTH), moves.get(1)); }</programlisting> <para> As the tests become more complex, we pull some of the detail out into helper methods to make the code more legible: </para> <programlisting> public void testMoveALongWay() { final Position DESTINATION = new Position(34, 71); robot.setCurrentPosition(new Position(0, 0)); robot.goto(DESTINATION); assertEquals("Should be destination", DESTINATION, robot.getPosition()); assertEquals("Should be same moves", makeExpectedLongWayMoves(), robot.getRecentMoves()); }</programlisting> <para> where <methodname>makeExpectedLongWayMoves()</methodname> returns a list of the moves we expect the robot to make for this test. </para> <para> There are problems 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 one of these tests fail, 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. Second, to test this behaviour at all, we had to add some functionality to the real code, 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>. Third, although it's not the case here, test suites based on extracting history from objects tend to need lots of utilities for constructing and comparing collections of values. The need to write external code to manipulate an object is often a warning that its class is incomplete and that some behaviour should be moved from the utility to the class. </para> <para> Is there a better way? Can we find a style that will give me better error reporting and put the behaviour in the right place? </para> </section> <!-- Simple Unit Tests --> <section> <title>Factoring Out The Motor</title> <para> 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 where it has to go, and it has to move along the route it has chosen. If we separate these responsibilities into two objects, we break out the tests that go with them: that the route planning object creates a suitable route and that the robot moving object follows that route correctly. What's a good name for a robot moving object? How about <emphasis>Motor</emphasis>? If we leave the route planning in the <classname>Robot</classname> object, we can intercept the requests between the <classname>Robot</classname> and its <classname>Motor</classname> to see what's happening inside. We'll start by defining an interface for <interfacename>Motor</interfacename>. We know that there must be some kind of request, which we'll call <methodname>move()</methodname>, that takes a move request as a parameter. We don't yet know what's in that request, so we'll define an empty <interfacename>MoveRequest</interfacename> interface as a placeholder to get us through the compiler. </para> <programlisting> public interface Motor { void move(MoveRequest request); }</programlisting> <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.</para> <para>I can now write my tests to create a Robot with an implementation of the <interfacename>Motor</interfacename> 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:</para> <example><title>Testing using a mock motor</title> <programlisting>public void testGotoSamePlace() { final Position POSITION = new Position(0, 0); robot.setCurrentPosition(POSITION); Motor mockMotor = new Motor() { public void move(MoveRequest request) { fail("There should be no moves in this test"); } }; robot.setMotor(mockMotor); robot.goTo(POSITION); assertEquals("Should be same", POSITION, robot.getPosition()); }</programlisting> </example> <para>In this test, if there is a bug in the Robot code and the Motor gets requested to move, the mock implementation of <methodname>move()</methodname> will fail immediately and stop the test; I no longer need to ask the Robot where it's been.</para> <para>Now I know that my Robot class works I can write a real implementation of the Motor interface:</para> <example><title>A real implementation of the Motor interface</title> <programlisting> public class OneSpeedMotor implements Motor { public void move(MoveRequest request) { turnBy(request.getTurn()); advance(request.getDistance()); } [...] } </programlisting> </example> <para>As my tests grow, I can refactor the various mock implementations into a single, more sophisticated MockMotor and use it throughout all the Robot tests; for example:</para> <example><title>Creating a mock motor class</title> <programlisting>public void MockMotor implements Motor { private ArrayList expectedRequests = new ArrayList(); public void move(MoveRequest request) { assert("Too many requests", this.expectedRequests.size() > 0); assertEquals("Should be next request", this.expectedRequests.remove(0), request); } public void addExpectedRequest(MoveRequest request) { this.expectedRequests.add(request); } public void verify() { assertEquals("Too few requests", 0, this.expectedRequests.size()); } }</programlisting> </example> <para>Which makes our tests look like:</para> <example><title>Testing our robot with mock motors</title> <programlisting>public class TestRobot { [...] static final Position ORIGIN = new Position(0, 0); public void setUp() { mockMotor = new MockMotor(); robot = new Robot(mockMotor); robot.setCurrentPosition(ORIGIN); } public void testGotoSamePlace() { robot.goTo(ORIGIN); assertEquals("Should be same", ORIGIN, robot.getPosition()); mockMotor.verify(); } public void testMoveOnePoint() { final Position DESTINATION = new Position(1, 0); mockRobot.addExpectedRequest(new MoveRequest(1, MoveRequest.SOUTH)); robot.goto(DESTINATION); assertEquals("Should be destination", DESTINATION, robot.getPosition()); mockMotor.verify(); } public void testMoveALongWay() { final Position DESTINATION = new Position(34, 71); mockMotor.addExpectedRequests(makeExpectedLongWayMoveRequests()); robot.goto(DESTINATION); assertEquals("Should be destination", DESTINATION, robot.getPosition()); mockMotor.verify(); } } </programlisting> </example> </section> <!-- Factoring out the motor --> <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 <methodname>getRecentMoveRequests()</methodname> 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> <example><title>Tracking distance with the decorator pattern</title> <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 [...] println("Total distance was: " + motorTracker.getTotalDistance()); </programlisting> </example> <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>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. </para> </section> <!-- Conclusions --> </chapter> |