Retrofitting StoryQ onto existing unit tests
StoryQ is great for BDD-ifying existing unit tests, and that's what this page explains how to do -
however it's not always a good idea to try and retrofit BDD onto existing unit tests. BDD is an agile
methodology that is all about multi-stakeholder communication, and usually tests things outside-in (while a lot of unit tests are at the slight lower "unit" level). That said, if you know what you're doing, here's how you retrofit StoryQ onto a unit test:
The original unit test
Here's a simple unit test that checks how well a
System.Collections.Generic.Dictionary with
string keys works in case insensitive mode (using
StringComparer.InvariantCultureIgnoreCase). We are testing a system class here for demonstration purposes only, usually your own code will be the system under test.
[TestMethod]
public void CaseInsensitiveRetrievalFromDictionary()
{
Dictionary<string, int> d = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
d["five"] = 5;
Assert.IsTrue(d.ContainsKey("FIVE"));
Assert.AreEqual(5, d["FIVE"]);
}
1) Identify the Given / When / Then sections of your unit test
Given, When and Then are equivalent to
Arrange, Act and Assert. If you are lucky, your unit test will already be in that order (whether by design or naturally).
Put comments into your test that separate chunks of code into "given"s, "when"s and "then"s (use "and" if there's multiple chunks), and put in a human-friendly description of these cases. In this case each chunk is a single line, but in many cases the stages of your test may span many lines:
[TestMethod]
public void CaseInsensitiveRetrievalFromDictionary()
{
// Given a case insenstive dictionary
Dictionary<string, int> d = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
// When I put 5 into the dictionary with a key of "five"
d["five"] = 5;
// Then the dictionary should contain the key "FIVE"
Assert.IsTrue(d.ContainsKey("FIVE"));
// And the value in the dictionary for "FIVE" should be 5
Assert.AreEqual(5, d["FIVE"]);
}
2) Extract steps into methods
Each chunk of code can be expressed as a narrative. In StoryQ, each narrative is expressed as a method. So you just need to extract each chunk into a method. You can use Visual Studio's extract method tool (Ctrl+R, Ctrl+M) (or your favourite refactoring plugin) to do this. Note that you'll have to promote the dictionary into a class variable so that all the methods can access it. Also: you should leave off the prefixes "Given", When", "Then" and "And":
private Dictionary<string, int> _d;
[TestMethod]
public void TestCaseInsensitiveRetrievalFromDictionary()
{
// Given a case insenstive dictionary
ACaseInsensitiveDictionary();
// When i put 5 into the dictionary with a key of "five"
IPutSomethingIntoTheDictionaryWithALowerCaseKey();
// Then the dictionary should contain the key "FIVE"
TheDictionaryShouldContainTheUpperCaseKey();
// And the value in the dictionary for "FIVE" should be 5
TheValueInTheDictionaryForTheUpperCaseKeyShouldBeTheSame();
}
private void ACaseInsensitiveDictionary()
{
_d = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
}
private void IPutSomethingIntoTheDictionaryWithALowerCaseKey()
{
_d["five"] = 5;
}
private void TheDictionaryShouldContainTheUpperCaseKey()
{
Assert.IsTrue(_d.ContainsKey("FIVE"));
}
private void TheValueInTheDictionaryForTheUpperCaseKeyShouldBeTheSame()
{
Assert.AreEqual(5, _d["FIVE"]);
}
3) Wrap the narrative method calls in StoryQ's warm embrace
Because each of these methods takes no arguments and returns void, you can pass them into StoryQ whenever StoryQ expects an executable narrative. First, however, we have to write the non-executable part of the Story. This is an extremely important step in BDD, because it documents the justification for the feature being tested:
new Story("Custom IEqualityComparers on .NET Dictionaries")
.InOrderTo("Retrieve data from a dictionary regardless of the string key's case")
.AsA("Developer")
.IWant("To create a dictionary with a case-insensitive IEqualityComparer")
.WithScenario("Storing and Retrieving values with StringComparer.InvariantCultureIgnoreCase")
Once this is provided, we should start using the narrative methods as arguments to the StoryQ fluent interface. Start by replacing:
// Given a case insenstive dictionary
ACaseInsensitiveDictionary();
with:
.Given(ACaseInsensitiveDictionary)
You should end up with the following StoryQ test method. Don't forget to call Execute at the end:
[TestMethod]
public void TestCaseInsensitiveRetrievalFromDictionary()
{
new Story("Custom IEqualityComparers on .NET Dictionaries")
.InOrderTo("Retrieve data from a dictionary regardless of the string key's case")
.AsA("Developer")
.IWant("To create a dictionary with a case-insensitive IEqualityComparer")
.WithScenario("Storing and Retrieving values with StringComparer.InvariantCultureIgnoreCase")
.Given(ACaseInsensitiveDictionary)
.When(IPutSomethingIntoTheDictionaryWithALowerCaseKey)
.Then(TheDictionaryShouldContainTheUpperCaseKey)
.And(TheValueInTheDictionaryForTheUpperCaseKeyShouldBeTheSame)
.Execute();
}
Running this should produce the nicely formatted results:
Story is Custom IEqualityComparers on .NET Dictionaries
In order to Retrieve data from a dictionary regardless of the string key's case
As a Developer
I want To create a dictionary with a case-insensitive IEqualityComparer
With scenario Storing and Retrieving values with StringComparer.InvariantCultureIgnoreCase
Given a case insensitive dictionary => Passed
When I put something into the dictionary with a lower case key => Passed
Then the dictionary should contain the upper case key => Passed
And the value in the dictionary for the upper case key should be the same => Passed
4) Parameterize your Narrative methods
A final step that you might like to take is to make your Narrative methods accept parameters. This makes them more reusable, and can improve the readability. We've show an example where the method
TheDictionaryShouldContainTheKey_ is reused:
private Dictionary<string, int> _d;
[TestMethod]
public void TestCaseInsensitiveRetrievalFromDictionary()
{
new Story("Custom IEqualityComparers on .NET Dictionaries")
.InOrderTo("Retrieve data from a dictionary regardless of the string key's case")
.AsA("Developer")
.IWant("To create a dictionary with a case-insensitive IEqualityComparer")
.WithScenario("Storing and Retrieving values with StringComparer.InvariantCultureIgnoreCase")
.Given(ACaseInsensitiveDictionary)
.When(IPut_IntoTheDictionaryWithAKeyOf_, 5, "five")
.Then(TheDictionaryShouldContainTheKey_, "FIVE")
.And(TheDictionaryShouldContainTheKey_, "FiVe")
.And(TheValueInTheDictionaryFor_ShouldBe_, "FIVE", 5)
.Execute();
}
private void TheValueInTheDictionaryFor_ShouldBe_(string key, int expected)
{
Assert.AreEqual(expected, _d[key]);
}
private void TheDictionaryShouldContainTheKey_(string key)
{
Assert.IsTrue(_d.ContainsKey(key));
}
private void IPut_IntoTheDictionaryWithAKeyOf_(int value, string key)
{
_d[key] = value;
}
private void ACaseInsensitiveDictionary()
{
_d = new Dictionary<string, int>(StringComparer.InvariantCultureIgnoreCase);
}
This will result in the following output:
Story is Custom IEqualityComparers on .NET Dictionaries
In order to Retrieve data from a dictionary regardless of the string key's case
As a Developer
I want To create a dictionary with a case-insensitive IEqualityComparer
With scenario Storing and Retrieving values with StringComparer.InvariantCultureIgnoreCase
Given a case insensitive dictionary => Passed
When I put 5 into the dictionary with a key of five => Passed
Then the dictionary should contain the key FIVE => Passed
And the dictionary should contain the key FiVe => Passed
And the value in the dictionary for FIVE should be 5 => Passed
Hopefully, that kind of output is exactly what you're looking for. It's our belief that this is a pretty easy process to go through after a couple of attempts - at least when you've got a good "extract method" tool.
If you want to see the effect of a failing test, just change the dictionary declaration to a case-sensitive one:
private void ACaseInsensitiveDictionary()
{
_d = new Dictionary<string, int>();
}
Go back to the home page to continue reading