Often I need to write some complex code with multiple requirements that when stacked on top of each other require a larger cognitive load than I care for. Along with its other benefits, Test-driven Development makes it easier and quicker to write code of this nature.

For example, lets say we need to write some code that will convert a fashion brand, like Off-White, L’Afshar, or Issey Miyake, into a hashtag. Hashtags have rules, and in our case the requirements will be:

  • They should start with a #
  • They should not have any punctuation
  • They should not have any spaces
  • They should be in lower case

Writing the tests

First, let’s create a test project using xUnit. We’ll also add my favorite assertion library, Shouldly, not only because I prefer the syntax, but because it does a great job of describing why tests fail.

dotnet new xunit -o Test
cd Test
dotnet add package Shouldly

We’ll create a new class (likely outside our test project) that will contain our hashtag generator logic and, for now, its only method will just return whatever was passed to it.

public class HashtagGenerator
{
    public string Generate(string input)
    {
        return input;
    }
}

Rather than put any thought into how we should implement that method, instead we’ll take a number of brand examples and convert them into hashtags ourselves.

Brand Hashtag
Off-White #offwhite
L’Afshar #lafshar
Issey Miyake #isseymiyake

We’ll then use the Theory and InlineData xUnit attributes to write test methods that will map directly to my hashtag requirements and using our example brands with their expected hashtags. I found a number of other brand examples I want to ensure the code will cater for so we’ll those as well.

public class HashtagGeneratorTests
{
    HashtagGenerator _generator = new HashtagGenerator();

    [Theory]
    [InlineData("Off-White", "#offwhite")]
    [InlineData("L'Afshar", "#lafshar")]
    [InlineData("P.A.R.O.S.H.", "#parosh")]
    [InlineData("Nº21", "#n21")]
    [InlineData("RED(V)", "#redv")]
    public void Should_strip_punctuation(string input, string expected)
    {
        _generator.Generate(input).ShouldBe(expected);
    }

    [Theory]
    [InlineData("K Jacques", "#kjacques")]
    [InlineData("Issey Miyake", "#isseymiyake")]
    [InlineData("A.W.A.K.E. Mode", "#awakemode")]
    public void Should_strip_spaces(string input, string expected)
    {
        _generator.Generate(input).ShouldBe(expected);
    }

    [Theory]
    [InlineData("AQUAZZURA", "#aquazzura")]
    [InlineData("Missoni", "#missoni")]
    public void Should_transform_to_lowercase(string input, string expected)
    {
        _generator.Generate(input).ShouldBe(expected);
    }

    [Theory]
    [InlineData("Givenchy", "#givenchy")]
    [InlineData("Birkenstock", "#birkenstock")]
    public void Should_prefix_with_hash(string input, string expected)
    {
        _generator.Generate(input).ShouldBe(expected);
    }
}

For each InlineData, it’s parameters will be passed to the method the attribute adorns. This means that each test method will be called once per InlineData. Notice too that the test methods are identical - only the test name changes - each one representing a requirement. 12 InlineData means 12 tests.

Running the tests, expectedly, results in 12 failures. I’ve --CUT-- the output for brevity, but already we can see the value that Shouldly gives us as it explains why our tests didn’t pass.

dotnet test
--CUT--
Starting test execution, please wait...
--CUT--
  X Test.HashtagGeneratorTests.Should_strip_punctuation(input: "Off-White", expected: "#offwhite") [1ms]
  Error Message:
   Shouldly.ShouldAssertException : _generator.Generate(input)
    should be
"#offwhite"
    but was
"Off-White"
    difference
Difference     |  |    |         |    |                       
               | \|/  \|/       \|/  \|/                      
Index          | 0    1    2    3    4    5    6    7    8    
Expected Value | #    o    f    f    w    h    i    t    e    
Actual Value   | O    f    f    -    W    h    i    t    e    
Expected Code  | 35   111  102  102  119  104  105  116  101  
Actual Code    | 79   102  102  45   87   104  105  116  101  
  Stack Trace:
     at Test.HashtagGeneratorTests.Should_strip_punctuation(String input, String expected) in /Users/staffordwilliams/git/coach_bags/src/Test/HashtagGeneratorTests.cs:line 20

--CUT--

Test Run Failed.
Total tests: 12
     Failed: 12
 Total time: 3.1424 Seconds

Implementing the requirements

Now that the tests are written, it’s time to implement the requirements. Because we’re lazy, but also 😎, we’ll automate future test runs while we write the code.

dotnet watch test
watch : Waiting for a file to change before restarting dotnet...

Right off the bat, we’ll implement two requirements: lower case and hash prefix.

public class HashtagGenerator
{
    public string Generate(string input)
    {
        var transformed = input.ToLower();
        return $"#{transformed}";
    }
}

The moment we save, dotnet watch triggers another test run and, we’ve got four passing tests!

Total tests: 12
     Passed: 4
     Failed: 8

Now we need to strip punctuation and, like all good programmers, we presume someone has already written that code. As such, we type the following into google:

.net strip everything except letters and numbers

The first result is a 10 year old stackoverflow question and we unabashedly copy paste the highest voted answer directly into our code.

public class HashtagGenerator
{
    public string Generate(string input)
    {
        Regex rgx = new Regex("[^a-zA-Z0-9 -]");
        var transformed = rgx.Replace(input, "")
            .ToLower();

        return $"#{transformed}";
    }
}

On save, four more tests are passing, leaving us with only four failing tests. One of the failing tests however is from our strip punctuation requirement.

Error Message:
   Shouldly.ShouldAssertException : _generator.Generate(input)
    should be
"#offwhite"
    but was
"#off-white"

Taking a closer look at the regex from the code we pasted, we notice a hyphen at the end of the pattern. Removing the hyphen results in the test passing! It also draws our inquisitive eye to the space at the end of the pattern, so we remove that also and our last requirement is met - all tests pass!

public class HashtagGenerator
{
    public string Generate(string input)
    {
        Regex rgx = new Regex("[^a-zA-Z0-9]");
        var transformed = rgx.Replace(input, "")
            .ToLower();

        return $"#{transformed}";
    }
}
Test Run Successful.
Total tests: 12
     Passed: 12

Conclusion

The finished result is an implementation with great test coverage. The real examples that the tests use give us high confidence that the code works as expected and, that we weren’t breaking the implementation as we edited code to implement additional requirements. The tests were trivial to create and, were we to make breaking changes in the future, we’ll quickly see both the requirement and the reason we’ve created a bug. The benefits of using this process and its lowered cognitive load can make Test-driven Development an easier, quicker and safer way to write code.