Consider the following problem: We have two integer ranges (1,5) and (10,15) and we want to find the range of the gap between them, if any (6,9).
Lets start with a new project and add references to a test framework (NUnit) and a BDD framework (FluentAssert)
We’ll start with a simple container to hold an integer range:
public class Range
{
public int Start;
public int End;
}
Next we’ll stub the range gap finder
public class RangeGapFinder
{
}
and our test fixture
[TestFixture]
public class When_asked_for_the_gap_between_two_ranges
{
}
Now we can start driving out the implementation. First off, let’s check the happy path – a gap between the ranges:
[Test]
public void Given_a_value_gap()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(A_value_gap)
.Should(Get_a_non_null_result)
.Should(Get_the_correct_range_start_value)
.Should(Get_the_correct_range_end_value)
.Verify();
}
now to add the test parts
public void A_value_gap()
{
_range1 = new Range(1, 5);
_range2 = new Range(10, 15);
_expectedStart = 6;
_expectedEnd = 9;
}
private void Asked_to_find_the_gap(RangeGapFinder rangeGapFinder)
{
_gap = rangeGapFinder.FindGap(_range1, _range2);
}
private void Get_a_non_null_result()
{
_gap.ShouldNotBeNull();
}
private void Get_the_correct_range_start_value()
{
_gap.Start.ShouldBeEqualTo(_expectedStart);
}
private void Get_the_correct_range_end_value()
{
_gap.End.ShouldBeEqualTo(_expectedEnd);
}
which implies this method stub
public class RangeGapFinder
{
public Range FindGap(Range range1, Range range2)
{
return null;
}
}
and requires a constructor for the Range container
public class Range
{
public int Start;
public int End;
public Range(int start, int end)
{
Start = start;
End = end;
}
}
Run the test. It fails. Now change the implementation so that the test passes.
public Range FindGap(Range range1, Range range2)
{
return new Range(range1.End + 1, range2.Start - 1);
}
That takes care of the happy path. Now what should happen if the ranges overlap (there is no gap) e.g. (1,5) and (2,9)? Let’s say the implementation should return null if there is no gap. First the failing test:
[Test]
public void Given_second_range_starts_in_the_first_range()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(Second_range_starting_in_the_first_range)
.Should(Get_a_null_result)
.Verify();
}
private void Second_range_starting_in_the_first_range()
{
_range1 = new Range(1, 5);
_range2 = new Range(2, 9);
}
private void Get_a_null_result()
{
_gap.ShouldBeNull();
}
Run the test and it fails. Now to update the implementation.
public Range FindGap(Range range1, Range range2)
{
if (range1.End > range2.Start)
{
return null; // no gap
}
return new Range(range1.End + 1, range2.Start - 1);
}
That covers the overlapping no-gap case. So now let’s think about inputs that could break the code. The first case is if the end value of range1 is the same as the start value of range2 e.g. (1,5) and (5,9). There is no gap so it should return null right?
[Test]
public void Given_first_range_End_value_equal_to_second_range_Start_value()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(First_range_End_value_same_as_second_range_Start_value)
.Should(Get_a_null_result)
.Verify();
}
private void First_range_End_value_same_as_second_range_Start_value()
{
_range1 = new Range(1, 5);
_range2 = new Range(5, 9);
}
Run the test and it fails! The fix is simple enough:
public Range FindGap(Range range1, Range range2)
{
if (range1.End >= range2.Start)
{
return null; // no gap
}
return new Range(range1.End + 1, range2.Start-1);
}
Okay. Another no-gap case is when the two ranges meet but do not overlap e.g. (1,5) (6,10). That should return null too. Does it?
[Test]
public void Given_adjacent_ranges()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(Adjacent_ranges)
.Should(Get_a_null_result)
.Verify();
}
private void Adjacent_ranges()
{
_range1 = new Range(1, 5);
_range2 = new Range(6, 10);
}
Run the test and it fails. The fix is another adjustment to the guard clause:
public Range FindGap(Range range1, Range range2)
{
if (range1.End + 1 >= range2.Start)
{
return null; // no gap
}
return new Range(range1.End + 1, range2.Start - 1);
}
What else could happen? The user could call the method with the ranges out of order e.g. (6,10) and (1,5). In this case let’s just do what the user expected instead of throwing an exception.
[Test]
public void Given_range2_Start_value_lower_than_range1_Start_value()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(Second_range_Start_lower_than_first_range_Start)
.Should(Get_a_non_null_result)
.Should(Get_the_correct_range_start_value)
.Should(Get_the_correct_range_end_value)
.Verify();
}
private void Second_range_Start_lower_than_first_range_Start()
{
_range1 = new Range(10, 15);
_range2 = new Range(1, 5);
_expectedStart = 6;
_expectedEnd = 9;
}
Test fails. Now to fix it we’ll just recurse:
public Range FindGap(Range range1, Range range2)
{
if (range2.Start < range1.Start)
{
return FindGap(range2, range1);
}
if (range1.End + 1 >= range2.Start)
{
return null; // no gap
}
return new Range(range1.End + 1, range2.Start - 1);
}
What else could happen? The user might send the start and end values swapped. A range is a range so let’s do the expected thing there too, starting with range1:
[Test]
public void Given_range1_End_value_lower_than_its_Start_value()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(Range1_end_value_lower_than_its_start_value)
.Should(Get_a_non_null_result)
.Should(Get_the_correct_range_start_value)
.Should(Get_the_correct_range_end_value)
.Verify();
}
private void Range1_end_value_lower_than_its_start_value()
{
_range1 = new Range(5, 1);
_range2 = new Range(10, 15);
_expectedStart = 6;
_expectedEnd = 9;
}
The test fails. We could simply update the Range
if (range1.End < range1.Start)
{
int temp = range1.End;
range1.End = range1.Start;
range1.Start = range1.End;
}
but that introduces a side effect. Let’s update the test to take the potential side effect into account:
[Test]
public void Given_range1_End_value_lower_than_its_Start_value()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(Range1_end_value_lower_than_its_start_value)
.Should(Get_a_non_null_result)
.Should(Get_the_correct_range_start_value)
.Should(Get_the_correct_range_end_value)
.Should(Not_change_the_values_of_range1)
.Should(Not_change_the_values_of_range2)
.Verify();
}
private void Not_change_the_values_of_range1()
{
_range1.Start.ShouldBeEqualTo(_originalRange1.Start);
_range1.End.ShouldBeEqualTo(_originalRange1.End);
}
private void Not_change_the_values_of_range2()
{
_range2.Start.ShouldBeEqualTo(_originalRange2.Start);
_range2.End.ShouldBeEqualTo(_originalRange2.End);
}
private void Asked_to_find_the_gap(RangeGapFinder rangeGapFinder)
{
_originalRange1 = new Range(_range1.Start, _range1.End);
_originalRange2 = new Range(_range2.Start, _range2.End);
_gap = rangeGapFinder.FindGap(_range1, _range2);
}
Now the fix is create a new Range with the points swapped.
public Range FindGap(Range range1, Range range2)
{
if (range2.Start < range1.Start)
{
return FindGap(range2, range1);
}
if (range1.End < range1.Start)
{
range1 = new Range(range1.End, range1.Start);
}
if (range1.End + 1 >= range2.Start)
{
return null; // no gap
}
return new Range(range1.End + 1, range2.Start - 1);
}
Next we do the same for range2:
[Test]
public void Given_range2_End_value_lower_than_its_Start_value()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(Range2_end_value_lower_than_its_start_value)
.Should(Get_a_non_null_result)
.Should(Get_the_correct_range_start_value)
.Should(Get_the_correct_range_end_value)
.Should(Not_change_the_values_of_range1)
.Should(Not_change_the_values_of_range2)
.Verify();
}
private void Range2_end_value_lower_than_its_start_value()
{
_range1 = new Range(1, 5);
_range2 = new Range(15, 10);
_expectedStart = 6;
_expectedEnd = 9;
}
and implement the fix:
public Range FindGap(Range range1, Range range2)
{
if (range2.Start < range1.Start)
{
return FindGap(range2, range1);
}
if (range1.End < range1.Start)
{
range1 = new Range(range1.End, range1.Start);
}
if (range2.End < range2.Start)
{
range2 = new Range(range2.End, range2.Start);
}
if (range1.End + 1 >= range2.Start)
{
return null; // no gap
}
return new Range(range1.End + 1, range2.Start - 1);
}
One last thing. We need some null checks on the inputs. Instead of retuning null we’ll make these exceptional cases. We could just let the first use trigger a NullReferenceException but a more meaningful exception might be nice.
[Test]
public void Given_a_null_for_range1()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(A_null_range1)
.ShouldThrowException<ArgumentNullException>("range1 cannot be null")
.Verify();
}
private void A_null_range1()
{
_range1 = null;
_range2 = new Range(1, 5);
}
private void Asked_to_find_the_gap(RangeGapFinder rangeGapFinder)
{
if (_range1 != null)
{
_originalRange1 = new Range(_range1.Start, _range1.End);
}
_originalRange2 = new Range(_range2.Start, _range2.End);
_gap = rangeGapFinder.FindGap(_range1, _range2);
}
public Range FindGap(Range range1, Range range2)
{
if (range1 == null)
{
throw new ArgumentNullException("range1","range1 cannot be null");
}
...
And the same for range2:
[Test]
public void Given_a_null_for_range2()
{
Test.Given(new RangeGapFinder())
.When(Asked_to_find_the_gap)
.With(A_null_range2)
.ShouldThrowException<ArgumentNullException>("range2 cannot be null")
.Verify();
}
private void A_null_range2()
{
_range1 = new Range(1, 5);
_range2 = null;
}
private void Asked_to_find_the_gap(RangeGapFinder rangeGapFinder)
{
if (_range1 != null)
{
_originalRange1 = new Range(_range1.Start, _range1.End);
}
if (_range2 != null)
{
_originalRange2 = new Range(_range2.Start, _range2.End);
}
_gap = rangeGapFinder.FindGap(_range1, _range2);
}
with final result:
public Range FindGap(Range range1, Range range2)
{
if (range1 == null)
{
throw new ArgumentNullException("range1","range1 cannot be null");
}
if (range2 == null)
{
throw new ArgumentNullException("range2", "range2 cannot be null");
}
if (range2.Start < range1.Start)
{
return FindGap(range2, range1);
}
if (range1.End < range1.Start)
{
range1 = new Range(range1.End, range1.Start);
}
if (range2.End < range2.Start)
{
range2 = new Range(range2.End, range2.Start);
}
if (range1.End + 1 >= range2.Start)
{
return null; // no gap
}
return new Range(range1.End + 1, range2.Start - 1);
}
That’s it.
Read Full Post »