Mars Rover – Implementing Rover : Moving Forward
Welcome to the fifth installment of Learning Through Example – Mars Rover! In this post, we’ll pick up from where we left on with Rover
and start digging into how to make it move forward! We’ll first examine what it means for the Rover
to move forward by looking over the requirements in deeper detail. Once we have a better understanding, we’ll start driving out the functionality by focusing on a simple case and building more complexity. By the end of this post, we’ll have a Rover
that will know how to move forward when facing any Direction
!
So What Does It Mean To Move Forward?
If we look back at the original requirements for moving forward, we find this single line with regards to moving forward
When the rover is told to move forward, then it will move one rover unit in the direction it’s facing
Super helpful, right? I don’t know about you, but this not nearly enough information for us to start our work because I’m not sure what that actually means!
In this case, we will have a more in-depth conversation with our Subject Matter Expert and we’ll find out that depending on the Orientation
of the Rover
the Rover
‘s Location
will change. Through additional conversations, we end up figuring out some more concrete business rules for when the Rover
moves forward.
- Given the
Rover
is facing North, when it moves forward, then itsY
value increases by 1 - Given the
Rover
is facing South, when it moves forward, then itsY
value decreases by 1 - Given the
Rover
is facing East, when it moves forward, then itsX
value increases by 1 - Given the
Rover
is facing East, when it moves forward, then itsX
value decreases by 1
Great, we have enough information to get started so we can start demonstrating the software and get quicker feedback!
Writing The First Test
Let’s begin writing our first test for the Rover
moving forward! We’ll be leveraging the same naming guidelines mentioned in part three to help make the use cases standout in our tests
So far, so good! The test matches the intent behind the name and a new developer can see that we’ve created a Rover
facing North
, called its MoveForward
method and making sure that the Y
property is 1.
If we try running the test, it will fail because Rover
doesn’t have a MoveForward
method, so let’s go ahead and write a simple implementation.
Even though there’s no business logic implemented, it’s enough code for our test to compile and if we run the test, the test fails because Y
is not 1.
With that in mind, let’s go ahead and write the simplest thing that could work just to check our approach.
Hmm, when we try to compile this code though, we get the following error
What’s going on here?
When Things Go Off Track
The error is caused due to the interaction of struct
and the Location
property implementation. If you recall, struct
s are value types which means when you assign them to a variable, the variable has its own copy of the struct
, not the reference.
So what does that have to do with the Location
property? Well, we’ve defined it as an auto-property which is syntactic sugar for telling the compiler to generate a backing field for the property and to implement default get
and set
logic.
So the problem arises from how get
is working. It’s returning the backing field which is going to be stored as a variable for use. Recall from above that when we do that type of assignment, we’re working on a copy of the value. So if we try to make changes to the copy, the changes won’t make it back to the backing field which in turn won’t ever update the Location
property!
Getting Back On Track
So good job on the compiler letting us know that there’s a problem even if the message is a bit obscure! But how do we fix the problem? Well, to get the code to compile, instead of updating the Y
property of Location
, let’s go ahead and update the entire Location
property instead.
And if we try to run our test, we find that it now passes, hooray!
Refactoring The Code
For those keeping track at home, we’re doing a pretty good job of following Test Drive Development (TDD) principles in that we first wrote a failing test, then wrote enough code to make it pass. The third step is to refactor our code (both production and test) to make it easier to work with or to make it more robust.
If we take a look at the MoveForward
method, it’s pretty simple and there’s not much we can refactor there for now.
Is Our State Correct?
So if our business code is pretty good, let’s take a look at our test and see what can be done.
Looking at this code, one thing that stands out is that we’re checking that Y
got updated, but we’re not verifying that X
nor the Orientation
didn’t change. In fact, if we change the implementation of MoveForward
to set the X value to be 200 and change the Orientation
to be South
, the test would still pass and it clearly shouldn’t!
Thankfully, we can mediate this oversight by creating an expectedLocation
which will have the expected X
and Y
values for the Rover. In addition, we’ll update one Assert
to use this new value and add another Assert
to verify the Orientation
Nice! We’re now much more explicit about our expectations of Rover
should be at this exact Location
and should have this exact Orientation
, otherwise, fail the test.
Are There Hidden Assumptions?
While looking at this test, there’s one more subtle issue with code, can you spot it?
We’re making an assumption about what the initial Location
for the Rover
! What if the Rover
started off at (5, 5) instead of (0, 0)? This test would fail, but not for the right reason (an error in the production code), but due to fragility in the way the test was written.
If we wanted to harden this test, we have two approaches
Setting the Location
We could change our Arrange
step to explicitly set the initial location of Rover
to be (0, 0). This would guarantee the initial setup and if the default Location
were to ever change, our test would still pass.
Capturing the Initial Location
When we look at this test and the code we’re testing, the key thing that we’re wanting to test is that the right value was modified correctly (in this case either by +1 or -1). Given that, we could update our Arrange
step to capture what the initial Location
was and then update our Assert
step to know about the location.
Given the two approaches, I like the idea of capturing the initial location, so that’s what I’m going to go with.
Writing Additional Tests
Now that we have a passing test for Rover
and moving forward, let’s go ahead and implement another piece of functionality by writing a test for when the Rover
is facing South
Red/Green/Refactor for Rover Facing South
And write enough code to make it pass!
Now that we have a passing test suite again, is there anything we want to refactor? Are there any patterns starting to emerge?
From the business code, MoveForward
seems pretty straightforward and I’m not sure what refactor I could do there that would make a lot of sense right now.
If we take a look at the test code, I’m noticing that our two tests so far look almost like carbon copies of each other. In fact, if we take a closer look, it seems like the only differences between the two tests are the Rover
‘s Orientation
and the expectedLocation
. I’m really tempted to refactor this code to be a bit more DRY and remove some duplication. However, I’ve only seen two examples so far and before I refactor to a pattern, I actually want the pattern to manifest first so I know what the pattern is.
Let’s keep writing some more tests and see what pattern emerges!
Red/Green/Refactor for Rover Facing East
Now that the Rover
can move forward when facing North
or South
, let’s go ahead and write a test for when the Rover
faces East.
With the test in place, let’s write enough code to make it pass.
With this passing test, let’s take a look at possible refactoring opportunities.
Refactoring Coordinate
If we look at the production code, I’m getting really tired of having to write Location = new Coordinate {X=Location.X..., Y=Location.Y...}
because I know I’m going to have to write this similar logic for the last remaining test for moving forward and probably something similar for moving backward.
Looking at the way we’ve been modifying Coordinate
, it seems like we’re every modifying X
or Y
by a set amount, so what if we wrote some methods that could adjust either X
or Y
?
If we take a look at Coordinate
, it seems like we have a struct with the two properties in mention, so let’s add a method called AdjustXBy
that will return a new Coordinate
with X
adjusted by that value and keep Y
the same
With this change in place, let’s go ahead and update our MoveForward
method to use this new code!
Even in this small example, this addition is already more concise of our intent than the other two cases. After doing a quick verification that the test still passes (otherwise the refactor isn’t a refactor), let’s go ahead and add a new method to Coordinate
called AdjustYBy
that is similar to AdjustXBy
And let’s go ahead and update MoveForward
to take advantage of this new functionality
After making that much change to the production code, we’ll go ahead and run our test suite again and it seems like the change is working as expected, nice!
Refactoring Test Code
Now that we’ve refactored the business rules and our test suite is passing correctly, we can take a look at refactoring our test code. With the addition of the East
test, the tests are definitely following a pattern and I should be able to extract out that logic to a single test and then pass in different parameters (even though the link is to NUnit, most test frameworks support this concept).
Given the differences between the tests, we would need to extract the starting Direction
and the expectedLocation
to be parameters. However, the expectedLocation
is based on the initialLocation which is currently based on whatever the Rover
defaults to.
Based on that chain, if we wanted to do this refactor, we would have to pass in a Rover
as the parameter and I really don’t like that idea because if Rover
grows to be bigger, then creating a Rover
becomes more involved and I don’t want to inflict that onto my test. In addition, one thing that is nice about our tests is that they’re easy to read and to follow their logic which has a ton of value given that developers spend more time reading code than writing code.
All of that to say, that even though the tests look similar, I’m going to pass on refactoring to a single unified test because I’d be trading readability for removing duplication and these tests are small enough that I don’t think it’s that much technical debt to take on.
Red/Green/Refactor for Rover Facing West
With the latest test, we’re 3/4 of the way through implementing MoveForward
, so let’s go ahead and write another failing test for when the Rover
faces West
.
With the test in places, let’s write enough code to make the test pass by taking advantage of Coordinate.AdjustXBy
And with this latest addition, not only do we have a passing test suite, but we’ve also covered the business rules for when the Rover
moves forward, completing this part of the kata, nice!
As a recap, here’s what Rover
and WhenMovingForward
looks like
Wrapping Up
With this final test in place, we have the core functionality for when the Rover
moves forward. In addition, we’ve written enough tests and functionality now that if requirements were to change, we have a pretty good guess on what the work involved would be. In the next part of the kata, we’ll start implementing a new piece of functionality!