Sunday, February 14, 2010

Nice BDD Naming Convention For MSTest

If you like to use Behaviour Driven Development (like me :) then I assume you are using a naming convention for your tests, maybe like this:

- Given an empty shopping cart
- When I add an item
- Then shopping item count should be 1

I really like using the above naming convention due to it expressiveness – I can instantly see what a test is actually testing and the context under which that test is running. I’ve been doing some research into the best practise for using BDD naming for MSTest unit tests and found this blog post. The author has given a very neat example of how to use a base context class which you can then include in your tests. The idea is that you have the following class which sets out the basic structure of a test:

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTest
{
    public abstract class ContextSpecification
    {
        public TestContext TestContext { get; set; }

        [TestInitialize]
        public void TestInitialize()
        {
            Context();
            BecauseOf();
        }

        [TestCleanup]
        public void TestCleanup()
        {
            Cleanup();
        }

        protected virtual void Context()
        {
        }

        protected virtual void BecauseOf()
        {
        }

        protected virtual void Cleanup()
        {
        }
    }
}

Those of you who have used frameworks such as MSpec will find the above very familiar (I’m a big fan of MSpec and use it for some of my projects at work, but none of my colleagues us it, hence my need for finding an MSTest way of BDD’ing my unit tests). You have the usual MSTest related code (TestContext, TestInitialize, etc) declared in this class, but notice the Context, BecauseOf and Cleanup methods. We’ll take a look at how those are used now. Check out the following unit test:

namespace UnitTest
{
    public static class GetPersonNameSpecs
    {
        public class GetPersonNameSpecsContext : ContextSpecification
        {
            protected Person sut;
            protected string actual = String.Empty;
        }

        [TestClass]
        public class when_name_has_not_been_given : GetPersonNameSpecsContext
        {
            protected override void Context()
            {
                sut = new Person();
            }

            protected override void BecauseOf()
            {
                actual = sut.GetFullName();
            }

            [TestMethod]
            public void returned_name_should_be_empty()
            {
                Assert.AreEqual(String.Empty, actual);
            }
        }

        [TestClass]
        public class when_name_has_been_given : GetPersonNameSpecsContext
        {
            protected override void Context()
            {
                sut = new Person("Jim");
            }

            protected override void BecauseOf()
            {
                actual = sut.GetFullName();
            }

            [TestMethod]
            public void returned_name_should_be_Jim()
            {
                Assert.AreEqual("Jim", actual);
            }
        }
    }
}

As per the suggestion of the blog author, I have a static class named GetPersonNameSpecs and inside that class is the real meat and bones – two test classes which contain the unit tests. See how I make use of the Context and BecauseOf methods - with Context I can do my pre-test initialisation (creating class instances, or if I was using a mock framework, creating mock objects), then in the BecauseOf method I invoke the code to be tested. The final phase of the test, the assertion, is what’s declared with a [TestMethod] attribute. Since in ContextSpecification both Context and BecauseOf are called during TestInitialize, there is no need for us to apply any attributes to our version of those methods.

The advantage of this structure is it’s readability, take a look at how the above tests look like in my test viewer window:

image

and in the results window:

image

Now, I don’t know about you, but I know that if I had to return to these tests in 6 months time, I will find it much easier to deal with.