Domain Driven TDD

By Mike on 26 April 2012

As I saw Greg Young's Simple Testing presentation, I just knew I HAVE TO USE that. Just to serve you a sample to peek your interest, Greg shows a way to do something very similar to BDD (but it's still TDD) without a complex framework.  So instead of having just a 'a_null_argument_throws' test passed, you also get context information in a human friendly way. And that with a basic setup which can be coded in a hour or so.

If you're gonna google for Greg's Simple Testing 'Framework', you'll find that it contains a lot of code which doesn't resemble much the code he presented on the slides. But that's not a problem since the concepts are the most valuable bits.

So I made my own setup as it can't be called a framework, not even a toolkit, it contains basically 2 classes and one of them is just to prettify things. Long story short, the idea is to have a more BDDish way of writing tests but with minimum of effort and maximum of info. That is having this

public class Page_State:Fixture<Fulldot.Domain.Common.BasicPage>
   {
       public Page_State()
       {
           _model = Given.AnExistingPostOrPage();
       }
       [Expect]
       public void it_cant_be_published()
       { 
          _model.InAnyStateExcept(PageState.Published).And().WithoutContent();
           Assert.False(_model.CanBePublished);
       }
}

shows this

Scenario: Page State
Given:
    an existing post or page in any state except Published and without content
Then:
    it cant be published

And that just using a standard testing framework, in my case xUnit.Net. Note the [Expect] attribute, it's just a class extending the [FactAttribute] of xUNit. I'm using it for the whole purpose of showing the test name with the underscores replaced to spaces. Yeas, that's it.

The Fixture<T> class features some complex code

public abstract class Fixture<T> where T : class
    {
        protected T _model;
        public Fixture()
        {
            Console.WriteLine("Scenario: " + GetType().Name.Replace('_', ' '));
            Console.WriteLine("Given:");
           
        }
 }

Yep, that's it. The actually tricky part is not in this simple herlpers, but in how you setup the tests. And the secret is a Fluent approach.

The above sample is from the test units for the Fulldot blog engine. Let's dissect the code.

I setup the context in the constructor (standard in xUnit.net). 'Given.AnExistingPostOrPage()' communicates the initial context and it looks like this.

public static class Given
    {
        public static BasicPage AnExistingPostOrPage()
        {
            Console.Write("\tan existing post or page");
            var p = new Post(1);
            p.DisableEvents();
            p.AssignId(1);
            p.SetContent(New.CommonPageContent);
            p.EnableEvents();
            return p;
        }
}

The actual implementation is not important. What's important is that I setup the relevant object while providing information about it.

In the actual test I have ' _model.InAnyStateExcept(PageState.Published).And().WithoutContent();' . I think it's very easy to understand the intention of this code. And once again, I'm using a fluent approach.

public static BasicPage InAnyStateExcept(this BasicPage pg, PageState state)
        {
            Console.Write(" in any state except " + state);
            pg.DisableEvents();
           var s = PageState.Draft;
            switch(state)
            {
                case PageState.Draft:s=PageState.NeedsReview;
                    break;
                case PageState.Published:
                case PageState.NeedsReview: s = PageState.Draft;
                    break;
            }
          pg.ChangeStateTo(s);
            pg.EnableEvents();
            return pg;
        }
public static BasicPage WithoutContent(this BasicPage pg)
        {
            Console.Write(" without content");
            var cmd = New.CreatePostCommand;
            pg.DisableEvents();
            cmd.Content = null;
            pg.SetContent(cmd);
            pg.EnableEvents();
            return pg;
        }

I think by now it's obvious why it can't be made into a generic framework: the fluent interface depends on your domain. Or how Greg put it: "it is a subset of the Ubiquitous Language". This also means that this approach works very well when you're testing the Domain. For simple assertions you just use the 'normal' tests. You won't lose any value.  In fact, this style makes the most sense when testing complex use cases, for the majority of the situations it's overkill.

A few notes:

  • This fluent TDD has the obvious benefit of providing self-explaining tests. It greatly reduces the time you need to READ and UNDERSTAND the tests 6 months later. The non-programmers can read the output too.
  • It makes you think better about the domain objects and helps you design them according to the use cases. 
  • It has an upfront cost because you need to spend more time in order to properly structure the code.
  • But you do get more maintainable code.