July 4, 2012

Using BDD specs for unit testing

Recently I decided that I wanted to publish a lot of the code from my startup Fabrik as open source. Rather than dumping the code on GitHub, I want to properly document the behaviour of this code using BDD specs.

Let’s look at an example, a regular expression in the Fabrik.Common.RegexUtils class.

/// <summary>
/// A regular expression for validating slugs.
/// </summary>
public static readonly Regex SlugRegex = new Regex(@"[^a-z0-9\s-]");

Here’s how we could test this using NUnit and traditional style unit tests:

[TestFixture]
public class RegexTests2 
{
    [Test]
    public void Slug_regex_should_not_match_invalid_slug()
    {
        Assert.IsFalse(RegexUtils.SlugRegex.IsMatch("This isn't cool man!"));
    }

    [Test]
    public void Slug_regex_should_match_valid_slug()
    {
        Assert.IsTrue(RegexUtils.SlugRegex.IsMatch("this-is-valid"));
    }
}

And here’s the (not so useful) output:

2 passed, 0 failed, 0 skipped, took 0.55 seconds (NUnit 2.6.0).

Of course, running via the NUnit test runner will give you something a little more useful (I’m using TDD.net).

Now let’s look at how we could verify the same behaviour using BDD style specs:

public class RegexTests
{
    [Subject("Using Slug Regular Expression")]
    public class SlugRegex
    {
        static bool result;
        
        public class When_the_string_contains_invalid_characters
        {
            Because of = () =>
                result = RegexUtils.SlugRegex.IsMatch("This // is \\ a test?!");

            It Should_not_match = ()
                => result.ShouldBeFalse();
        }

        public class When_the_string_is_a_valid_slug
        {
            Because of = () =>
                result = RegexUtils.SlugRegex.IsMatch("this-is-a-test");

            It Should_match = ()
                => result.ShouldBeTrue();
        }
    }
}

Running these specs via the MSpec runner gives us the following output:

Slug Regular Expression, When the string contains invalid characters
- Should not match

Slug Regular Expression, When the string is a valid slug
- Should match

The output is much better since it actually describes each scenario (using the Slug Regular Expression), the context (When the string contains invalid characters) and the specification (It Should not match).

However, it does seem that this comes at the price of quite a bit extra code. Is it really worth it for this type of test?

For the above assertions, probably not. The problem is, you can’t guarantee that these are the only behaviours you’ll ever want to test. BDD really comes into its own when you start needing to add context to a test.

In the above example we had a number of “edge cases” that had cropped up in the past that we wanted to test. Using traditional unit tests would have just resulted in several separate unit tests. The point here is that these tests are related. We are testing a single behaviour (using Regular Expression) but under several contexts.

The new specs can be seen below:

public class RegexTests
{
	[Subject("Slug Regular Expression")]
    public class SlugRegex
    {
        static bool result;
        
        // ... as before

        // edge cases

        public class When_the_string_has_leading_or_trailing_whitespace
        {
            Because of = () =>
                result = RegexUtils.SlugRegex.IsMatch(" this-is-a-test ");

            It Should_not_match = ()
                => result.ShouldBeFalse();
        }

        public class When_the_string_has_leading_or_trailing_hyphens
        {
            Because of = () =>
                result = RegexUtils.SlugRegex.IsMatch("-this-is-a-test-");

            It Should_not_match = ()
                => result.ShouldBeFalse();
        }

        public class When_the_string_is_uppercase
        {
            Because of = () =>
                result = RegexUtils.SlugRegex.IsMatch("THIS-IS-A-TEST");

            It Should_not_match = ()
                => result.ShouldBeFalse();
        }
    }
}

These specs showed me that the original Regular Expression was not up to scratch. After a few iterations we arrived at a solution that met all our specifications:

/// <summary>
/// A regular expression for validating slugs.
/// Does not allow leading or trailing hypens or whitespace
/// </summary>
public static readonly Regex SlugRegex = new Regex(@"(^[a-z0-9])([a-z0-9-]+)([a-z0-9])$");

The MSpec output:

Slug Regular Expression, When the string contains invalid characters
- Should not match

Slug Regular Expression, When the string is a valid slug
- Should match

Slug Regular Expression, When the string has leading or trailing whitespace
- Should not match

Slug Regular Expression, When the string has leading or trailing hyphens
-Should not match

Slug Regular Expression, When the string is uppercase
- Should not match

For me this is a much better way of verifying behaviour, even in “simple” unit tests. Add to this that we can generate a pretty HTML MSpec report for our end user (in this case, other developers) and I think we have a win win situation.

When you think about unit tests in terms of testing behaviour, there are not many scenarios that couldn’t be described with a BDD story.

I’m still learning BDD but am already finding that writing tests has become easier. Instead of thinking “what should I test”, all I do is think about the behaviour of the thing I am testing. If I put something into a function (scenario) what should I get out (specification).

One final point I would like to make (especially to those learning BDD) is there is no “one-way” to write BDD specs. When I’m testing a domain model I may have a large number of scenarios with many contexts, since we have lots of behaviour to verify.

If I’m doing a simple unit test, I may just have a scenario without a context (or at least, there is only one possible context). This is often the case when testing parameterless methods:

[Subject("Calling Foo")]
public class Calling_foo
{
	It Should_say_bar;
}

public void Foo() 
{
	return "Bar";
}

Don’t get hung up on how you’re structuring your specs - that’ll come in time. It’s far better to be testing some behaviour that non at all.

© 2022 Ben Foster