I was reading Scott Bellwares article on Behavior-Driven Development, and was inspired to try out his framework called specunit-net"
So I took a kata from tddproblems and tried it out, I took the Roman number conversion problem:
So first I implemented a simple spec base class:
[TestFixture]
[Concern("base test")]
public abstract class behaves_like_context_with_Converter : ContextSpecification
{
protected RomanNumberConverter _converter;
protected override void Context()
{
_converter = new RomanNumberConverter();
}
protected void ShouldReturn(string romanNumber, int integerNumber)
{
int result;
result = _converter.Convert(romanNumber);
result.ShouldEqual(integerNumber);
}
}
Then I added specs for one char at a time, I started with I (one) and moved up, but reSharper re-structures this stuff:
[Concern("passed only one character")]
public class when_passed_one_character : behaves_like_context_with_Converter
{
[Observation]
[Test]
public void should_return_fifty()
{
ShouldReturn("L", 50);
}
[Observation]
[Test]
public void should_return_five()
{
ShouldReturn("V", 5);
}
[Observation]
[Test]
public void should_return_five_hundred()
{
ShouldReturn("D", 500);
}
[Observation]
[Test]
public void should_return_one()
{
ShouldReturn("I", 1);
}
[Observation]
[Test]
public void should_return_one_hundred()
{
ShouldReturn("C", 100);
}
[Observation]
[Test]
public void should_return_one_thousand()
{
ShouldReturn("M", 1000);
}
[Observation]
[Test]
public void should_return_ten()
{
ShouldReturn("X", 10);
}
}
So I started with the simplest implementation to make it work (unfortunately I didn’t save all the intermediate states so I can only show the end result), and followed that pragma all the way through, only implementing the simplest possible code to make it work.
Here is the end result, I cannot say that it is perfect, but i behaves like the spec says it should:
public class RomanNumberConverter
{
public int Convert(string s)
{
s = s.Trim();
int result = 0;
for (int position = 0; position < s.Length; position++)
{
if (position > 0 && !IsPreviousCharSmallerThanOrEqualToCurrent(s, position))
{
result += ConvertOneCharacter(s[position]) - ConvertOneCharacter(s[position - 1]);
result -= ConvertOneCharacter(s[position - 1]);
}
else
result += ConvertOneCharacter(s[position]);
}
return result;
}
private bool IsPreviousCharSmallerThanOrEqualToCurrent(string s, int position)
{
char previous = s[position - 1];
char current = s[position];
if (ConvertOneCharacter(previous) >= ConvertOneCharacter(current))
return true;
return false;
}
public int ConvertOneCharacter(char s)
{
Dictionary<char, int> romanValues = new Dictionary<char, int>
{
{'I', 1},
{'V', 5},
{'X', 10},
{'L', 50},
{'C', 100},
{'D', 500},
{'M', 1000}
};
if (romanValues.ContainsKey(s))
{
return romanValues[s];
}
throw new ArgumentException(s + " is not a roman number");
}
}
At the bottom you find all the rest of the specs.
[Concern("passed only two simple characters")]
public class when_passed_two_simple_character : behaves_like_context_with_Converter
{
[Observation]
[Test]
public void should_return_II()
{
ShouldReturn("II", 2);
}
[Observation]
[Test]
public void should_return_XV()
{
ShouldReturn("XV", 15);
}
}
[Concern("passed only three simple characters")]
public class when_passed_three_simple_character : behaves_like_context_with_Converter
{
[Observation]
[Test]
public void should_return_XVI()
{
ShouldReturn("XVI", 16);
}
}
[Concern("passed only values below ten")]
public class when_passed_values_below_ten : behaves_like_context_with_Converter
{
[Observation]
[Test]
public void should_return_IV()
{
ShouldReturn("IV", 4);
}
[Observation]
[Test]
public void should_return_IX()
{
ShouldReturn("IX", 9);
}
[Observation]
[Test]
public void should_return_VI()
{
ShouldReturn("VI", 6);
}
[Observation]
[Test]
public void should_return_VIII()
{
ShouldReturn("VIII", 8);
}
}
[Concern("passed only one characterwhen passed spaces")]
public class when_passed_spaces : behaves_like_context_with_Converter
{
[Observation]
[Test]
public void should_return_trim()
{
ShouldReturn("II ", 2);
}
}
[Concern("when passed invalid character")]
public class when_passed_invalid_character : behaves_like_context_with_Converter
{
[Observation]
[Test]
[ExpectedException(typeof (ArgumentException))]
public void should_throw()
{
_converter.Convert("P");
}
}
[Concern("when passed large numbers")]
public class when_passed_large_numbers : behaves_like_context_with_Converter
{
[Observation]
[Test]
public void should_return_1900()
{
ShouldReturn("MCM ", 1900);
}
[Observation]
[Test]
public void should_return_1974()
{
ShouldReturn("MCMLXXIV", 1974);
}
[Observation]
[Test]
public void should_return_1998()
{
ShouldReturn("MCMXCVIII", 1998);
}
[Observation]
[Test]
public void should_return_2008()
{
ShouldReturn("MMVIII", 2008);
}
}