Let's start by importing the existing Git repository.
Let's start setting up the project:
You will be presented with a screen similar to the following:
buld.gradle
file. Once that is done, you'll see that some classes and corresponding tests are already created:Imagine that a colleague of yours started working on this project. He's a good programmer and a TDD practitioner, and you trust his abilities to have a good test code coverage. In other words, you can rely on his work. However, that colleague did not finish the application before he left for his vacations and it's up to you to continue where he stopped. He created all the helper classes: Direction
, Location
, Planet
, and Point
. You'll notice that the corresponding test classes are there as well. They have the same name as the class they're testing with the Spec suffix (that is, DirectionSpec
). The reason for using this suffix is to make clear that tests are not intended only to validate the code, but also to serve as executable specification.
On top of the helper classes, you'll find the Ship
(implementation) and ShipSpec
(specifications/tests) classes. We'll spend most of our time in those two classes. We'll write tests in ShipSpec
and then we'll write the implementation code in the Ship
class (just as we did before).
Since we already learned that tests are not only used as a way to validate the code, but also as executable documentation, from this moment on, we'll use the phrase specification or spec instead of test.
Every time we finish writing a specification or the code that implements it, we'll run gradle test
either from the command prompt or by using the Gradle projects IDEA Tool Window:
With project setup, we're ready to dive into the first requirement.
We need to know what the current location of the ship is in order to be able to move it. Moreover, we should also know which direction it is facing: north, south, east, or west. Therefore, the first requirement is the following:
Before we start working on this requirement, let's go through helper classes that can be used. The Point
class holds the x
and y
coordinates. It has the following constructor:
public Point(int x, int y) { this.x = x; this.y = y; }
Similarly, we have the Direction enum
class with the following values:
public enum Direction { NORTH(0, 'N), EAST(1, 'E'), SOUTH(2, 'S'), WEST(3, 'W'), NONE(4, 'X'), }
Finally, there is the Location
class that requires both of those classes to be passed as constructor arguments:
public Location(Point point, Direction direction) { this.point = point; this.direction = direction; }
Knowing this, it should be fairly easy to write a test for this first requirement. We should work in the same way as we did in the previous chapter.
Try to write specs by yourself. When done, compare it with the solution in this book. Repeat the same process with the code that implements specs. Try to write it by yourself and, once done, compare it with the solution we're proposing.
The specification for this requirement can be the following;
@Test public class ShipSpec { public void whenInstantiatedThenLocationIsSet() { Location location = new Location( new Point(21, 13), Direction.NORTH); Ship ship = new Ship(location); assertEquals(ship.getLocation(), location); } }
This was an easy one. We're just checking whether the Location
object we're passing as the Ship
constructor is stored and can be accessed through the location
getter.
The implementation of this specification should be fairly easy. All we need to do is set the constructor argument to the location
variable:
public class Ship { private final Location location; public Location getLocation() { return location; } public Ship(Location location) { this.location = location; } }
The full source can be found in the req01-location branch of the tdd-java-ch04-ship
repository (https://bitbucket.org/vfarcic/tdd-java-ch04-ship/branch/req01-location).
We know that we'll need to instantiate Ship
for every spec, so we might as well refactor the specification class by adding the @BeforeMethod
annotation. The code can be the following:
@Test public class ShipSpec { private Ship ship; private Location location; @BeforeMethod public void beforeTest() { Location location = new Location( new Point(21, 13), Direction.NORTH); ship = new Ship(location); } public void whenInstantiatedThenLocationIsSet() { // Location location = new Location( // new Point(21, 13), Direction.NORTH); // Ship ship = new Ship(location); assertEquals(ship.getLocation(), location); } }
No new behavior has been introduced. We just moved part of the code to the @BeforeMethod
annotation in order to avoid duplication, which would be produced by the rest of the specifications that we are about to write. Now, every time a test is run, the ship object will be instantiated with location
as the argument.
Now that we know where our ship is, let's try to move it. To begin with, we should be able to go both forward and backward.
The Location helper
class already has the forward
and backward
methods that implement this functionality:
public boolean forward() { ... }
What should happen when, for example, we are facing north and we move the ship forward? Its location on the y
axis should decrease. Another example would be that when the ship is facing east, it should increase its x
axis location by one.
The first reaction can be to write specifications similar to the following two:
public void givenNorthWhenMoveForwardThenYDecreases() { ship.moveForward(); assertEquals(ship.getLocation().getPoint().getY(), 12); } public void givenEastWhenMoveForwardThenXIncreases() { ship.getLocation().setDirection(Direction.EAST); ship.moveForward(); assertEquals(ship.getLocation().getPoint().getX(), 22); }
We should create at least two more specifications related to cases where a ship is facing south and west.
However, this is not how unit tests should be written. Most people new to UT fall into the trap of specifying the end result that requires the knowledge of the inner workings of methods, classes, and libraries used by the method that is being specified. This approach is problematic on many levels.
When including external code in the unit that is being specified, we should take into account, at least in our case, the fact that the external code is already tested. We know that it is working since we're running all the tests every time any change to the code is done.
Rerun all the tests every time the implementation code changes.
This ensures that there is no unexpected side-effect caused by code changes.
Every time any part of the implementation code changes, all tests should be run. Ideally, tests are fast to execute and can be run by a developer locally. Once code is submitted to the version control, all tests should be run again to ensure that there was no problem due to code merges. This is especially important when more than one developer is working on the code. Continuous Integration tools such as Jenkins, Hudson, Travind, Bamboo and Go-CD should be used to pull the code from the repository, compile it, and run tests.
Another problem with this approach is that if an external code changes, there will be many more specifications to change. Ideally, we should be forced to change only specifications directly related to the unit that will be modified. Searching for all other places where that unit is called from might be very time-consuming and error prone.
A much easier, faster, and better way to write specification for this requirement would be the following:
public void whenMoveForwardThenForward() { Location expected = location.copy(); expected.forward(); ship.moveForward(); assertEquals(ship.getLocation(), expected); }
Since Location already has the forward
method, all we'd need to do is to make sure that the proper invocation of that method is performed. We created a new Location
object called expected
, invoked the forward
method, and compared that object with the location of the ship after its moveForward
method is called.
Note that specifications are not only used to validate the code, but are also used as executable documentation and, most importantly, as a way to think and design. This second attempt specifies more clearly what the intent is behind it. We should create a moveForward
method inside the Ship
class and make sure that the location.forward
is called.
With such a small and clearly defined specification, it should be fairly easy to write the code that implements it:
public boolean moveForward() { return location.forward(); }
Now that we have a forward movement specified and implemented, the backward movement should almost be the same:
public void whenMoveBackwardThenBackward() { Location expected = location.copy(); expected.backward(); ship.moveBackward(); assertEquals(ship.getLocation(), expected); }
Just like the specification, the backward movement implementation is just as easy:
public boolean moveBackward() { return location.backward(); }
The full source code for this requirement can be found in the req02-forward-backward
branch of the tdd-java-ch04-ship
repository (https://bitbucket.org/vfarcic/tdd-java-ch04-ship/branch/req02-forward-backward).
Moving the ship only back and forth, wont, get us far. We should be able to stir by moving it left and right as well.
After implementing the previous requirement, this one should be very easy since it can follow the same logic. The Location
helper class already contains the turnLeft
and turnRight
methods that perform exactly what is required by this requirement. All we need to do is integrate them into the Ship
class.
Using the same guidelines as those we have used so far, the specification for turning left can be the following:
public void whenTurnLeftThenLeft() { Location expected = location.copy(); expected.turnLeft(); ship.turnLeft(); assertEquals(ship.getLocation(), expected); }
You probably did not have a problem writing the code to pass the previous specification:
public void turnLeft() { location.turnLeft(); }
Turning right should be almost the same as turning left:
public void whenTurnRightThenRight() { Location expected = location.copy(); expected.turnRight(); ship.turnRight(); assertEquals(ship.getLocation(), expected); }
Finally, let's finish this requirement by implementing the specification for turning right:
public void turnRight() { location.turnRight(); }
The full source for this requirement can be found in the req03-left-right
branch of the tdd-java-ch04-ship
repository (https://bitbucket.org/vfarcic/tdd-java-ch04-ship/branch/req03-left-right).
Everything we have done so far was fairly easy since there were helper classes that provided all the functionality. This exercise was to learn how to stop attempting to test the end outcome and focus on a unit we're working on. We are building trust; we had to trust the code done by others (the helper classes). Starting from this requirement, you'll have to trust the code you wrote by yourself. We'll continue in the same fashion. We'll write specification, run tests, and see them fail; we'll write implementation, run tests, and see them succeed; finally, we'll refactor if we think the code can be improved. Continue thinking how to test a unit (method) without going deeper into methods or classes that the unit will be invoking.
Now that we have individual commands (forward, backward, left, and right) implemented, it's time to tie it all together. We should create a method that will allow us to pass any number of commands as a single string. Each command should be a character with f meaning forward, b being backward, l for turning left, and r for turning right.
Let's start with the command argument, that only has the f
(forward) character:
public void whenReceiveCommandsFThenForward() { Location expected = location.copy(); expected.forward(); ship.receiveCommands("f"); assertEquals(ship.getLocation(), expected); }
This specification is almost the same as the whenMoveForwardThenForward
specification except that, this time, we're invoking the ship.receiveCommands("f")
method.
We already spoke about the importance of writing the simplest possible code that passes the specification.
Write the simplest code to pass the test. This ensures a cleaner and clearer design and avoids unnecessary features
The idea is that the simpler the implementation, the better and easier it is to maintain the product. The idea adheres to the KISS principle. It states that most systems work best if they are kept simple rather than made complex; therefore, simplicity should be a key goal in design and unnecessary complexity should be avoided.
This is a good opportunity to apply this rule. You might be inclined to write a piece ofcode similar to the following:
public void receiveCommands(String commands) { if (commands.charAt(0) == 'f') { moveForward(); } }
In this example code, we are verifying whether the first character is f
and, if it is, invoking the moveForward
method. There are many other variations that we can do. However, if we stick to the simplicity principle, a better solution would be the following:
public void receiveCommands(String command) { moveForward(); }
This is the simplest and shortest possible code that will make the specification pass. Later on, we might end up with something closer to the first version of the code; we might use some kind of a loop or come up with some other solution when things become more complicated. As for now, we are concentrating on one specification at a time and trying to make things simple. We are attempting to clear our mind by focusing only on the task at hand.
For brevity, the rest of the combinations (b
, l
, and r
) are not presented below (continue to implement them by yourself). Instead, we'll jump to the last specification for this requirement.
Now that we are able to process one command (whatever that the command is), it is time to add the option to send a string of commands. The specification can be the following:
public void whenReceiveCommandsThenAllAreExecuted() { Location expected = location.copy(); expected.turnRight(); expected.forward(); expected.turnLeft(); expected.backward(); ship.receiveCommands("rflb"); assertEquals(ship.getLocation(), expected); }
This is a bit longer, but is still not an overly complicated specification. We're passing commands rflb
(right, forward, left, and backward) and expecting that the Location changes accordingly. As before, we're not verifying the end result (seeing whether the if coordinates have changed), but checking whether we are invoking the correct calls to helper methods.
The end result can be the following:
public void receiveCommands(String commands) { for (char command : commands.toCharArray()) { switch(command) { case 'f': moveForward(); break; case 'b': moveBackward(); break; case 'l': turnLeft(); break; case 'r': turnRight(); break; } } }
If you tried to write specifications and the implementation by yourself and if you followed the simplicity rule, you probably had to refactor your code a couple of times in order to get to the final solution. Simplicity is the key and refactoring is often a welcome necessity. When refactoring, remember that all specifications must be passing all the time.
Refactor only after all the tests have passed.
Benefits: refactoring is safe
If all the implementation code that can be affected has tests and if they are all passing, it is relatively safe to refactor. In most cases, there is no need for new tests; small modifications to existing tests should be enough. The expected outcome of refactoring is to have all the tests passing both before and after the code is modified.
The full source for this requirement can be found in the req04-commands
branch of the tdd-java-ch04-ship
repository (https://bitbucket.org/vfarcic/tdd-java-ch04-ship/branch/req04-commands).
Earth is a sphere as any other planet. When Earth is presented as a map, reaching one edge, wraps us to another, for example, when we move east and reach the furthest point in the Pacific Ocean, we are wrapped to the west side of the map and we continue moving towards America. Furthermore, to make the movement easier, we can define the map as a grid. That grid should have length and height expressed as an X and Y axis. That grid should have maximum length (X) and height (Y).
The first thing we can do is pass the Planet
object with the maximum X
and Y
axis coordinates to the Ship
constructor. Fortunately, Planet
is one more of the helper classes that are already made (and tested). All we need to do is instantiate it and pass it to the Ship
constructor:
public void whenInstantiatedThenPlanetIsStored() { Point max = new Point(50, 50); Planet planet = new Planet(max); ship = new Ship(location, planet); assertEquals(ship.getPlanet(), planet); }
We're defining the size of the planet as 50x50 and passing that to the Planet
class. In turn, that class is afterwards passed to the Ship
constructor. You might have noticed that the constructor needs an extra argument. In the current code, our constructor requires only Location
. To implement this specification, it should accept Planet
, as well.
How would you implement this specification without breaking any of the existing specifications?
Let's take a bottom-up approach. An assert requires us to have a planet
getter:
private Planet planet; public Planet getPlanet() { return planet; }
Next, the constructor should accept Planet
as a second argument and assign it to the previously added planet
variable. The first attempt might be to add it to the existing constructor, but that would break many existing specifications that are using a single argument constructor. This leaves us with only one option: a second constructor:
public Ship(Location location) { this.location = location; } public Ship(Location location, Planet planet) { this.location = location; this.planet = planet; }
Run all the specifications and confirm that are all successful.
Our specifications forced us to create the second constructor since changing the original one would break the existing tests. However, now that everything is green, we can do some refactoring and get rid of the single argument constructor. The specification class already has the beforeTest
method that is run before each test. We can move everything, but the assert itself to this method:
public class ShipSpec { … private Planet planet; @BeforeMethod public void beforeTest() { Point max = new Point(50, 50); location = new Location(new Point(21, 13), Direction.NORTH); planet = new Planet(max); // ship = new Ship(location); ship = new Ship(location, planet); } public void whenInstantiatedThenPlanetIsStored() { // Point max = new Point(50, 50); // Planet planet = new Planet(max); // ship = new Ship(location, planet); assertEquals(ship.getPlanet(), planet); } }
With this change, we effectively removed the usage of the Ship single argument constructor. By running all specifications, we should confirm that this change worked.
Now, with a single argument constructor that is not in use any more, we can remove it from the implementation class, as well:
public class Ship { … // public Ship(Location location) { // this.location = location; // } public Ship(Location location, Planet planet) { this.location = location; this.planet = planet; } … }
By using this approach, all specifications were green all the time. Refactoring did not change any existing functionality, nothing got broken, and the whole process was done fast.
Now, let's move into wrapping itself.
Like in other cases, the helper classes already provide all the functionality that we need. So far, we used the Location.forward
method without arguments. To implement wrapping, there is the overloaded Location.forward(Point max)
method that will wrap the location when we reach the end of the grid. With the previous specification, we made sure that Planet
is passed to the Ship
class and that it contains Point max
. Our job is to make sure that max
is used when moving forward. The specification can be the following:
/* The name of this method has been shortened due to line's length restrictions. The aim of this test is to check the behavior of ship when it is told to overpass the right boundary. */ public void overpassEastBoundary() { location.setDirection(Direction.EAST); location.getPoint().setX(planet.getMax().getX()); ship.receiveCommands("f"); assertEquals(location.getX(), 1); }
By now, you should be getting used to focusing on one unit at a time and to trust that those that were done before are working as expected. This implementation should be no different. We just need to make sure that the maximum coordinates are used when the location.forward
method is called:
public boolean moveForward() { // return location.forward(); return location.forward(planet.getMax()); }
The same specification and implementation should be done for the backward method. For brevity, it is excluded from this book, but it can be found in the source code.
The full source for this requirement can be found in the req05-wrap
branch of the tdd-java-ch04-ship
repository (https://bitbucket.org/vfarcic/tdd-java-ch04-ship/branch/req05-wrap).
We're almost done. This is the last requirement.
Even though most of the Earth is covered in water (approximately 70 percent), there are continents and islands that can be considered as obstacles for our remotely controlled ship. We should have a way to detect whether our next move would hit one of those obstacles. If such a thing happens, the move should be aborted and the ship should stay on the current position and report the obstacle.
The specifications and the implementation of this requirement are very similar to those we did previously, and we'll leave that to you.
Here are a few tips that can be useful:
Planet
object has the constructor that accepts a list of obstacles. Each obstacle is an instance of the Point
class.Location.foward
and Location.backward
methods have overloaded versions that accept a list of obstacles. They return true
if a move was successful and false
if it failed. Use this Boolean to construct a status report required for the Ship.receiveCommands
method.receiveCommands
method should return a string with the status of each command. 0
can represent OK and X
can be for a failure to move (OOXO = OK, OK, Failure, OK).The full source for this requirement can be found in the req06-obstacles
branch of the tdd-java-ch04-ship
repository (https://bitbucket.org/vfarcic/tdd-java-ch04-ship/branch/req06-obstacles).
3.145.101.109