Developing the remote-controlled ship

Let's start by importing the existing Git repository.

Project setup

Let's start setting up the project:

  1. Open IntelliJ IDEA. If an existing project is already opened, select File | Close Project.

    You will be presented with a screen similar to the following:

    Project setup
  2. To import the project from the Git repository, click on Check out from Version Control and select Git. Type https://bitbucket.org/vfarcic/tdd-java-ch04-ship.git in to the Git Repository URL field and click on Clone:
    Project setup
  3. Answer Yes when asked whether you would like to open the project. Next you will be presented with the Import Project from Gradle dialog. Click on OK:
    Project setup
  4. IDEA will need to spend some time downloading the dependencies specified in the buld.gradle file. Once that is done, you'll see that some classes and corresponding tests are already created:
    Project setup

Helper classes

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:

Helper classes

With project setup, we're ready to dive into the first requirement.

Requirement 1

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:

Note

You are given the initial starting point (x, y) of a ship and the direction (N, S, E, or W) it is facing.

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.

Specification

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.

Tip

The @Test annotation

When TestNG has the @Test annotation set on the class level, there is no need to specify which methods should be used as tests. In this case, all public methods are considered to be TestNG tests.

Specification implementation

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).

Refactoring

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.

Requirement 2

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.

Note

Implement commands that move the ship forward and backward (f and b).

The Location helper class already has the forward and backward methods that implement this functionality:

    public boolean forward() {
        ...
    }

Specification

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.

Tip

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.

Specification implementation

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();
    }

Specification

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);
    }

Specification implementation

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).

Requirement 3

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.

Note

Implement commands that turn the ship left and right (l and r).

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.

Specification

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);
    }

Specification implementation

You probably did not have a problem writing the code to pass the previous specification:

    public void turnLeft() {
        location.turnLeft();
    }

Specification

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);
    }

Specification implementation

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).

Requirement 4

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.

Note

The ship can receive a string with commands (lrfb is equivalent to left, right, forward, and backward).

Specification

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.

Specification implementation

We already spoke about the importance of writing the simplest possible code that passes the specification.

Tip

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.

Specification

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.

Specification implementation

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.

Tip

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).

Requirement 5

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).

Note

Implement wrapping from one edge of the grid to another.

Specification

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?

Specification implementation

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.

Refactoring

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.

Specification

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);
    }

Specification implementation

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).

Requirement 6

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.

Note

Implement surface detection before each move to a new position. If a command encounters a surface, the ship aborts the move, stays on the current position, and reports 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:

  • The Planet object has the constructor that accepts a list of obstacles. Each obstacle is an instance of the Point class.
  • The 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.
  • The 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).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.101.109