Learning Through Example

Mars Rover – Implementing Rover – Turn Right

Welcome to the ninth installment of Learning Through Example – Mars Rover! In this post, we’re going to start driving out the functionality for the Rover and the business rules for turning right! First, we’ll take a look at the requirements to make sure we have an idea of what’s needed. From there, we’ll start implementing the requirements. By the end of this post, we’ll have a Rover that can do perform all of the commands!

Turning in Place By Turning Right

If we look back at the original requirements for turning right, we find this single line as the requirement

When the rover is told to turn left, it will rotate 90 degrees to the right, but not change its location

Given this requirement, we’re able to double-check with our Subject Matter Expert that the Rover is essentially rotating in place which yields the following requirements.

  • Given the Rover is facing North, when it turns right, then the Rover should be facing East at the same Coordinate
  • Given the Rover is facing East, when it turns right, then the Rover should be facing South at the same Coordinate
  • Given the Rover is facing South, when it turns right, then the Rover should be facing West at the same Coordinate
  • Given the Rover is facing West, when it turns right, then the Rover should be facing North at the same Coordinate

So far, the requirements are very similar to when we were implementing TurnLeft, so let’s see if we can leverage the same setup!

Red/Green/Refactor For Rover Facing North

Given what we learned when we were implementing the rules for TurnLeft, I’m going to go ahead and copy the test and add the single case for when Rover faces North

[Test]
[TestCase(Direction.North, Direction.East, TestName = "AndFacingNorthThenTheRoverFacesEast")]
public void RoverTurningRight(Direction start, Direction expected)
{
var rover = new Rover { Orientation = start };
var initialLocation = rover.Location;
rover.TurnRight();
Assert.AreEqual(expected, rover.Orientation);
Assert.AreEqual(initialLocation, rover.Location);
}
view raw WhenTurningRight.cs hosted with ❤ by GitHub

Since TurnRight doesn’t exist, this test will fail so let’s go ahead and write enough code to make it pass. For the implementation, I’m going to go ahead and take advantage of the pattern that we found last time, yielding the following

public void TurnRight()
{
Action ifNorth = () => Orientation = Direction.East;
Action ifSouth = () => {};
Action ifEast = () => {};
Action ifWest = () => {};
Execute(ifNorth, ifSouth, ifEast, ifWest);
}
view raw RoverFacingNorth.cs hosted with ❤ by GitHub

One thing to note is to take a look at how we defined ifSouth, ifEast, and ifWest. Since we’re still wanting to implement one requirement at a time, I’ve defined them to do nothing so that if Execute is called, nothing will happen. Alternatively, I could have defined them to throw an exception, but I believe that’s a bit much since we’ll be implementing the functionality soon!

With that implementation, our test passes, so it’s time to go on to the next requirement!

Rapid Test Creation with TestCase

With a parameterized unit test in place, adding a new test is as simple as adding a new TestCase attribute.

[Test]
[TestCase(Direction.North, Direction.East, TestName = "AndFacingNorthThenTheRoverFacesEast")]
[TestCase(Direction.East, Direction.South, TestName = "AndFacingEastThenTheRoverFacesSouth")]
public void RoverTurningRight(Direction start, Direction expected)
{
var rover = new Rover { Orientation = start };
var initialLocation = rover.Location;
rover.TurnRight();
Assert.AreEqual(expected, rover.Orientation);
Assert.AreEqual(initialLocation, rover.Location);
}
view raw WhenTurningRight.cs hosted with ❤ by GitHub

And updating ifEast action in the TurnRight method

public void TurnRight()
{
Action ifNorth = () => Orientation = Direction.East;
Action ifSouth = () => {};
Action ifEast = () => Orientation = Direction.South;
Action ifWest = () => {};
Execute(ifNorth, ifSouth, ifEast, ifWest);
}
view raw RoverFacingEast.cs hosted with ❤ by GitHub

Now that we’ve proven how easy it is to make this change, let’s go ahead and knock out the final two test cases

[Test]
[TestCase(Direction.North, Direction.East, TestName = "AndFacingNorthThenTheRoverFacesEast")]
[TestCase(Direction.East, Direction.South, TestName = "AndFacingEastThenTheRoverFacesSouth")]
[TestCase(Direction.South, Direction.West, TestName = "AndFacingSouthThenTheRoverFacesWest")]
[TestCase(Direction.West, Direction.North, TestName = "AndFacingWestThenTheRoverFacesNorth")]
public void RoverTurningRight(Direction start, Direction expected)
{
var rover = new Rover { Orientation = start };
var initialLocation = rover.Location;
rover.TurnRight();
Assert.AreEqual(expected, rover.Orientation);
Assert.AreEqual(initialLocation, rover.Location);
}
view raw WhenTurningRight.cs hosted with ❤ by GitHub

And implement the code to make those cases pass

public void TurnRight()
{
Action ifNorth = () => Orientation = Direction.East;
Action ifSouth = () => Orientation = Direction.West;
Action ifEast = () => Orientation = Direction.South;
Action ifWest = () => Orientation = Direction.North;
Execute(ifNorth, ifSouth, ifEast, ifWest);
}
view raw RoverSouthAndWest.cs hosted with ❤ by GitHub

Goodness, that was a pretty quick implementation of TurnRight! Thanks to our previous refactoring in Rover and leveraging parameterized testing, we were able to implement this new feature with a minimal amount of code and the code we did write was focused on the new functionality, not boilerplate.

With that being said, here’s our final version of Rover!

public class Rover
{
public Direction Orientation { get; set; }
public Coordinate Location { get; set; }
public Rover()
{
Orientation = Direction.North;
Location = new Coordinate();
}
public void MoveForward()
{
Action ifNorth = () => Location=Location.AdjustYBy(1);
Action ifSouth = () => Location=Location.AdjustYBy(-1);
Action ifEast = () => Location=Location.AdjustXBy(1);
Action ifWest = () => Location=Location.AdjustXBy(-1);
Execute(ifNorth, ifSouth, ifEast, ifWest);
}
public void MoveBackward()
{
Action ifNorth = () => Location=Location.AdjustYBy(-1);
Action ifSouth = () => Location=Location.AdjustYBy(1);
Action ifEast = () => Location=Location.AdjustXBy(-1);
Action ifWest = () => Location=Location.AdjustXBy(1);
Execute(ifNorth, ifSouth, ifEast, ifWest);
}
public void TurnLeft()
{
Action ifNorth = () => Orientation = Direction.West;
Action ifWest = () => Orientation = Direction.South;
Action ifSouth = () => Orientation = Direction.East;
Action ifEast = () => Orientation = Direction.North;
Execute(ifNorth, ifSouth, ifEast, ifWest);
}
public void TurnRight()
{
Action ifNorth = () => Orientation = Direction.East;
Action ifEast = () => Orientation = Direction.South;
Action ifSouth = () => Orientation = Direction.West;
Action ifWest = () => Orientation = Direction.North;
Execute(ifNorth, ifSouth, ifEast, ifWest);
}
private void Execute(Action ifNorth, Action ifSouth, Action ifEast, Action ifWest)
{
switch(Orientation)
{
case Direction.North: ifNorth(); break;
case Direction.South: ifSouth(); break;
case Direction.East: ifEast(); break;
case Direction.West: ifWest(); break;
}
}
}
view raw RoverFinal.cs hosted with ❤ by GitHub

Wrapping Up

With the completion of TurnRight, the Rover has finally been implemented with all required pieces of functionality. Through this process, we learned how to write good unit tests, how to refactor unit tests to use parameterized unit testing, and reducing repetition through a SOLID refactor of if/else all of which culminating in TurnRight being a simple piece of functionality to include. In the next post, we will start working on implementing our logger!

Leave a Reply

%d bloggers like this: