Ships have Itineraries that have Ports - Walkthrough
Steps
- Discuss with your classmates how the domain model now looks.
- Create a new test file, which should
describea newItineraryobject. - Create a new test spec to check the new
Itineraryobject can be instantiated. - Write the code that makes this test pass.
- Create a new test spec to check the new
Itineraryobject has aportsproperty. - Write the code that makes this test pass.
- Refactor the
Shiptest suite so aShiptakes anItineraryobject instead of aPortobject. TheItineraryobject will have 2Portobjects stored in an array on itsportsproperty. - Refactor the
it can dock at a different porttest so that no argument is passed toship.dock, and asserts thecurrentPortto be the next port in theItineraryinstance. The tests will break. - Write the code that makes the tests pass again.
- Add, commit with a meaningful message, and push to GitHub.
Domain Model
The domain model might now look like this:
| Object | Methods | Properties |
|---|---|---|
| Ship | setSail | currentPort |
| dock | ||
| Port | ||
| Itinerary | ports |
Create a new test file Itinerary.test.js, which should describe Itinerary
:exclamation: You should be able to do this without the walkthrough. Once you've passed the test, then proceed.
Create a new test spec to check the new object can be instantiated.
:exclamation: You should be able to do this without the walkthrough. Once you've passed the test, then proceed.
Create a new test spec to check that an Itinerary instance can have ports
An Itinerary has ports. ports is plural which indicates an Itinerary will have one or more ports. We know know too that Port is its own entity. Therefore, when we instantiate an Itinerary, we will pass in an array of Port instances (you will need to require in your Port constructor!!!):

Here we:
- Setup: Create instances of
Port - Exercise: Pass
Portinstances into a new instance ofItinerary - Verify: Assert that our
Itineraryinstance does now possessPortinstances
Run your tests. They should fail with :red_circle: :
Write the code that makes this test pass.
The code to pass this is actually fairly straightforward:

As per the dependency inversion principle, our Itinerary object shouldn't know anything about a Port object. All it needs to know is that it takes an array which gets assigned to its ports property. So long as the interface of the object we pass in matches the interface of a Port object then we can swap out Port for another object without breaking the Itinerary object.
Modify your can set sail test to assert previousPort contains the previous port after setSail has been called
We need to ensure that when a ship sets sail, it sets a previousPort equal to the current port, so we know where we set sail from (and therefore can determine where the ship is next due to dock).
First let's modify our Ship test suite:
Here we've just added the following assertion:
You should of course now have failing tests, as a Ship instance doesn't yet have a previousPort property:
You can fix that now. First in your Ship constructor, add the property:
And then modify your setSail method to assign the current port to the previousPort property:
Your tests should now pass.
Refactoring the Ship test suite
A Ship starts off anchored :anchor: at a currentPort which is a Port instance passed into the Ship constructor. We now have additional abstraction in the form of our Itinerary object. We now need to refactor our Ship test suite so that a Ship takes an Itinerary instance and its currentPort is set to the first item in the Itinerary.
This is where OOP starts to become more complex. Up until now we've just been passing objects into other objects. Now we actually have to use the methods and properties on those objects within other objects.
Lets see how our Ship test suit might be modified to accommodate this additional abstraction. First in Ship.test.js require in the Itinerary object:
Then modify your has a starting port test changing it from:
To:
Remember that Itinerary expects ports. Even if we pass in a single port, we still have to ensure it's in an array, hence: new Itinerary([port]).
We also have to refactor the setSail test to ensure it takes an itinerary:
Change:
To:
And finally, refactor can dock at a different port so that it tests a ship docks at the next port in an itinerary, as opposed to the current behaviour of docking at a port we explicitly specify.
Change:
To:
Our user story dictates that a Ship should now dock at the next port on an Itinerary. Therefore it makes no sense to pass a Port into a Ship's dock method anymore as it's no longer our decision to make.
We do also have to call setSail now as dock no longer has a Port object passed in and therefore we need to ensure the previousPort is set so it knows where to dock next.
Take time to follow this through. You could spend an hour trying to work out what's going on, or you could take a day, but it is very important that you do grasp it. Remember, a Port goes into an Itinerary's ports, and this Itinerary gets passed to a ship (Port -> Itinerary -> Ship).
The tests will now fail with multiple errors:
Write the code that makes the tests pass
It's always good practice to start with the first error (at the top of your stack trace). Write the code that makes that one test pass, run the tests again, and move onto the next one.
Let's look at the first failing test:
To make this test pass, change:
To:
What's going on here? We pass an instance of Itinerary into the Ship constructor. Firstly, we assign it to an itinerary property so we keep access to it. The significant part is setting the current port though. An Itinerary object has ports, and we've passed it by reference into Ship. Therefore, it's perfectly acceptable to call ports on it still and to use square bracket notation to access the first array element (which of course is a Port).
Remember:
Port -> Itinerary -> Ship
Now if you run your tests again, you'll notice we've passed this test, but now we've created another failing test:

This is because in our Ship > can be instantiated test, we instantiate without an Itinerary instance passed, so Ship tries to set its currentPort property to undefined.ports[0]. We can fix this by modifying the test:

That should just leave us with one test failure remaining:

Remember, we now expect the dock method to pick the next Port on an Itinerary, therefore we aren't passing in a Port to the method anymore.
The pass this test, we need to change our dock method to set the currentPort to the next port in the itinerary. Remember at this point that setSail has been called already so we will need to use previousPort to work out where to dock next. Change the Ship dock method from:
to:
We get the index of the current port inside of the Itinerary and set the new current port to that index plus 1.
Test for the edge case that the ship can't set sail further than the last port in the itinerary. You should be testing that the setSail method throws an error when you try and sail past the last port in the itinerary.
Inside Ship.test.js add a new test spec:
This assertion syntax might seem unfamiliar. We are expecting our setSail method to throw an error the second time it is called. The problem is that if we do the following:
Then ship.setSail gets invoked and throws an error before the test has chance to assert it. Therefore, with the toThrowError matcher you always pass in a callback function so that Jest can decide when to call it.
This test should now fail:
Write the code that makes this test pass.
Change the Ship's setSail method to:
This will now cause another test to fail:
This is because we have other tests in our test suite where we call setSail with only one port in our itinerary. Therefore we need to go back and modify these tests (tedious I know, but it's what has to be done to have wonderful robust code!). To fix, add another port to the itinerary in the can set sail test: