4. Introducing a test harness.To be able to refactor Alarm, we first need to protect its current behavior (its check method) from regressions by writing tests for it.
The implicit dependency of Alarm on Sensor makes Alarm difficult to test. However, it's the fact that Sensor returns random values that makes Alarm impossible to test because the measured pressure values it gets are not deterministic.
It seems we're trapped in a vicious circle: in order to refactor the code (improving its design without altering its behavior) we must test it first, but in order to test it, we must change it first.
We can get out of this problem by applying a dependency-breaking technique called Extract and Override call.
4.1. Extract and Override call.This is a dependency-breaking technique from Michael Feather's Working Effectively with Legacy code book. These techniques consist of carefully making a very small modification in the production code in order to create a seam:
A seam is a place where you can alter behavior in your program without editing in that place.The behavior we want to test is the logic in Alarm's check method. This logic is very simple, just an if condition and a mutation of a property, but as we saw its dependence on Sensor makes it untestable.
To test it, we need to alter the collaboration between Alarm and Sensor so that it becomes deterministic. That would make Alarm testable. For that we have to create a seam first.
4.1.1. Extract call to create a seam.
First, we create a seam by extracting the collaboration with Sensor to a protected method, probeValue. This step must be made with a lot of caution because we have no tests yet.
Thankfully in Java, we can rely on the IDE to do it automatically.
4.1.2. Override the call in order to isolate the logic we want to test.
Next, we take advantage of the new seam, to alter the behavior of Alarm without affecting the logic we want to test.
To do it, we create a FakeAlarm class inside the AlarmShould tests that inherits from Alarm and overrides the call to the protected probeValue method: Now using FakeAlarm we are able to write all the necessary tests for Alarm's logic (its check method):
Now that we have a test harness for Alarm in place, we'll focus on making its dependency on Sensor explicit.
5. Making the dependency on Sensor explicit.Now our goal is to inject the dependency on Sensor into Alarm through its constructor.
To remain in the green all the time, we use the Parallel Change technique.
With TDD we drive a new constructor without touching the one already in place by writing a new behavior test with the help of a mocking library (mockito):
And we make the new test pass.
In the version of Alarm above, we also used the Parameterize Constructor technique to make the default constructor (used by FakeAlarm) depend on the new one.
Then we use the new constructor in the rest of the tests one by one in order to stop using FakeAlarm.
Once there are no test using FakeAlarm, we can delete it. This makes the default constructor become obsolete, so we delete it too.
Finally, we also inline the previously extracted probeValue method.
This is the resulting test code after introducing dependency injection in which we have also deleted the test used to drive the new constructor because we think it was redundant:
and this is the production code:
By making the dependency on Sensor explicit with dependency injection and using a mocking library we have simplified Alarm tests.
This is the second post in a series of posts about solving the Tire Pressure Monitoring System exercise in Java:
- Solving the Tire Pressure Monitoring System exercise (I)
- Solving the Tire Pressure Monitoring System exercise (II)
- Solving the Tire Pressure Monitoring System exercise (III)
- Solving the Tire Pressure Monitoring System exercise (IV)