|
From: Steve F. <sm...@us...> - 2001-10-27 23:37:09
|
Update of /cvsroot/mockobjects/mockobjects-java/doc/xdocs/papers
In directory usw-pr-cvs1:/tmp/cvs-serv4667
Modified Files:
brief_introduction.html
Log Message:
Changed example in introduction paper
Index: brief_introduction.html
===================================================================
RCS file: /cvsroot/mockobjects/mockobjects-java/doc/xdocs/papers/brief_introduction.html,v
retrieving revision 1.1
retrieving revision 1.2
diff -u -r1.1 -r1.2
--- brief_introduction.html 2001/09/24 23:40:22 1.1
+++ brief_introduction.html 2001/10/27 23:37:05 1.2
@@ -1,110 +1,225 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
-<title>Developing JDBC applications test-first</title>
-<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
-<style type="text/css">
+ <title>Developing JDBC applications test-first</title>
+
+ <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+
+ <style type="text/css">
<!--
.deemphasised { color: #666666}
h1 { text-align: center; font-family: Arial, Helvetica, sans-serif; font-weight: bold}
h3 { font-family: Arial, Helvetica, sans-serif; font-style: italic; font-weight: bold; font-size: small}
-.inline_code { font-family: "Courier New", Courier, mono; font-style: normal; font-size: smaller; vertical-align: middle}
-p { font-family: Arial, Helvetica, sans-serif}
+.inline_code { font-family: "Courier New", Courier, mono; font-style: normal; vertical-align: middle}
+p { font-family: Arial, Helvetica, sans-serif; margin-left: 5%}
li { font-family: Arial, Helvetica, sans-serif }
h2 { font-family: Arial, Helvetica, sans-serif; margin-top: 3%}
+pre { margin-left: 7%}
-->
</style>
</head>
-
-<body bgcolor="#FFFFFF">
-<h1>How Mock Objects happened</h1>
-<p align="center">Steve Freeman <tt> <st...@m3...></tt></p>
+ <body bgcolor="#ffffff">
+<h1>How Mock Objects happen</h1>
+<p align="Center">Steve Freeman <tt> <st...@m3...></tt></p>
<h2>Introduction</h2>
-<p>Unit testing is a fundamental practice in Extreme Programming, but most nontrivial
- code is difficult to test in isolation. You need to make sure that you test
- one feature at a time, and you want to be notified as soon as any problem occurs.
- Normal unit testing is hard because you are trying to test the code from outside.
-</p>
-<p>Mock Objects is an approach to writing unit tests based on two key techniques:
- replace everything except the code under test with mock implementations that
- emulate the rest of the environement, and put the test assertions <em>inside</em>
- those mock implementations so that you can validate the interactions between
- the objects in your test case.</p>
+
+<p>Unit testing is a fundamental practice in Extreme Programming, but
+most nontrivial code is difficult to test in isolation. You need to
+make sure that you test one feature at a time, and you want to be
+notified as soon as any problem occurs. Normal unit testing is hard
+because you are trying to test the code from outside. </p>
+
+<p>Mock Objects is an approach to writing unit tests based on two key
+techniques: replace everything except the code under test with mock
+implementations that emulate the rest of the environement, and put the
+test assertions <em>inside</em> those mock implementations so that you
+can validate the interactions between the objects in your test
+case.</p>
+
+<p>This article assumes that you are familiar with java and unit testing using JUnit</p>
+
<h2>Simple unit tests</h2>
-<p>Most people who start writing unit tests by calling some methods and checking
- some of the state of the object under test. For example, a test for a simple
- calculator object might be:</p>
-<pre> public void setUp() {
- this.calculator = new Calculator();
- }
-
- public void testAdd() {
- this.calculator.set(5);
- this.calculator.add(3);
- assertEquals("Should be addition", 8, this.calculator.getResult());
- }
-</pre>
-<p>As a class becomes more complex, this style of testing becomes harder to maintain.
- For example, if our calculator now keeps a history of the calculations it makes,
- a test might be:</p>
-<pre> public void testHistory() {
- this.calculator.set(5);
- this.calculator.add(3);
-
-
- Vector calcuations = new Vector();
- calcuations.add(new Calculation(Calculation.SET, 5));
- calcuations.add(new Calculation(Calculation.ADD, 3));
- assertEquals("Should be equal calculations",
- calcuations, this.calculator.getHistory());
-
- }
-</pre>
-<p>The problem with this approach is that we must either return the actual history
- data structure, which limits our choice of implementation, or return a vector
- copy, which is expensive. Furthermore, it is impossible to tell which choice
- we have made from the test. To hide the underlying collection, the next step
- is to return an <span class="inline_code">Iterator</span> for the calculation
- history, rather than the collection itself. Our test now becomes:</p>
-<pre> public void testHistory() {
- this.calculator.set(5);
- this.calculator.add(3);
-
-
- Vector calcuations = new Vector();
- calcuations.add(new Calculation(Calculation.SET, 5));
- calcuations.add(new Calculation(Calculation.ADD, 3));
- compareIterators(calculations.iterator(), this.calculator.getHistory());
- }</pre>
-<p>where <span class="inline_code">compareIterators()</span> is a helper method
- that compares the contents of two Iterators. </p>
-<p>This approach becomes less manageable as the code becomes still more complex.
- For example, we now change our class to perform some lengthy calculation that
- takes multiple input values and returns multiple output values:</p>
-<pre> public void testLongCalculation() {
- Result[] results =
- this.calculator.complicatedOperation(makeLongCalculationInputs());
-
-
- compareResults(makeLongCalculationResults(), results);
- }</pre>
-<p>To manage the inputs and results, there are helper methods, <span class="inline_code">makeLongCalculationInputs</span>
- and <span class="inline_code">makeLongCalculationResults</span>, for constructing
- the collections of values. </p>
-<p>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. When this test fails, we will have to step through the
- code to find the problem because the assertions are made <em>after</em> the
- calculation is finished. Is there a better way?</p>
-<h2>A first Mock Object</h2>
-<p>One thing we know about this complicated operation is that we need to accumulate
- a set of results that currently are returned from the method. We can factor
- out that process of accumulation to a separate object, and write an interface
- for it:</p>
-<pre> public interface Accumulator {
- /**<br> * called once for each result in the calculation
- */
- void add(Result result);<br> } </pre>
-<p> </p>
+
+<p>Most people start writing unit tests by creating an object, calling
+some methods on it, and then checking its state. For example, imagine
+that I'm writing a component automatically to direct robots through a
+warehouse. Given a start point on a grid, it will find a route through
+the stacks to a given destination. The tests will have to tell me that
+the robot arrives correctly and does not hit anything on the way. A
+first test might be: </p>
+
+<pre>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());
+ }
+}</pre>
+
+<p>This tells me that the robot thinks that it has arrived, but it
+doesn't tell me anything about how it got there. I would like to know
+that the Robot didn't move in this test. One option would be to store
+and retrieve the route it took after each <em>goto()</em>. For
+example:</p>
+
+<pre>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());
+}</pre>
+
+<p>My next test might be to move one point on the grid:</p>
+
+<pre>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));
+}</pre>
+
+<p>As the tests become more complex, I would pull out some of the
+detail into helper methods to make the code more legible:</p>
+
+<pre>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());
+}</pre>
+
+<p>Where <em>makeExpectedLongWayMoves()</em> returns another list of
+the moves I expect the robot to take for this test. </p>
+
+<p>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, I will have to
+step through the code to find the problem because the assertions have
+been made <em>after</em> the call has finished. Second, to test this
+behaviour at all, I had to add some functionality to the production
+class, namely retaining all the MoveRequests since the last
+<em>goto()</em>. I don't have any other immediate need for
+<em>getRecentMovesRequests()</em>. 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.</p>
+
+<p>Is there a better way? Can I find a style that will give me better
+error reporting and put the behaviour in the right place? </p>
+
+<h2>Factoring out the motor</h2>
+
+<p>One thing I can be sure of is that the Robot contains a Motor that
+responds to these move requests. If I can intercept those requests, I
+can track what's happening in the Robot. A first step would be to
+define an interface to the Motor:</p>
+
+<pre>public interface Motor {
+ void move(MoveRequest request);
+}</pre>
+
+<p>I can pass an instance of Motor to the Robot, perhaps via its
+constructor, and a simple implementation might be:</p>
+
+<pre>public class OneSpeedMotor implements Motor {
+ public void move(MoveRequest request) {
+ turnBy(request.getTurn());
+ advance(request.getDistance());
+ }
+ [...]
+}</pre>
+
+<p>I can now refactor my tests and replace the real Motor with a
+MockMotor that can watch what's happening in the Robot and complain as
+soon as something goes wrong. My first test is now:</p>
+
+<pre>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());
+}</pre>
+
+<p>In this test, if there is a bug in the Robot code and the Motor
+gets requested to move, the mock implementation of <em>move()</em>
+will simply fail and stop the test. I no longer need to ask the Robot
+where it's been. I can write a more sophisticated MockMotor that works
+for multiple requests and use it throughout all the Robot tests; for
+example:</p>
+
+<pre>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());
+ }
+}</pre>
+
+<p>Which makes our tests look like:</p>
+
+<pre>public class TestRobot {
+ [...]
+ static final Position ORIGIN = new Position(0, 0);
+ public void setUp() {
+ mockMotor = new MockMotor();
+ robot = new Robot(mockMotor);
+ robot.setCurrentPosition(POSITION);
+ }
+ 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();
+ }
+}</pre>
+
+<p>What has this change achieved?
+</p>
<hr>
<p>© Steve Freeman, 2001</p>
<p> </p>
|