Unit Tests: Simplifying test setup with Builders

Had some fun at work today. The web portal to Scheduler service is written in ASP.NET MVC4. As such we have a lot of controllers and of course there are unit tests that run on the controllers.

Now, while ASP.NET MVC4 apparently did have testability as a goal, it still requires quite a lot of orchestration to test controllers. Now all this orchestration and mock setups only muddies the waters and gets in the way test readability. By implication, tests are harder to understand, maintain and eventually becomes harder to trust the tests.

Let me give an example:

 [TestFixture]
public class AppControllerTests  {
    // private
    /// set up fields elided
    // elided

    [SetUp]
    public void Setup()
    {
        _mockRepo = new MockRepository(MockBehavior.Strict);
        _tenantRepoMock = _mockRepo.Create();
        _tenantMapRepoMock = _mockRepo.Create();
        _controller = MvcMockHelpers.CreatePartialMock(_tenantRepoMock.Object, _tenantMapRepoMock.Object);

        guid = Guid.NewGuid();

        // partial mock - we want to test controller methods but want to mock properties that depend on
        // the HTTP infra.
        _controllerMock = Mock.Get(_controller);
    }

    [Test]
    public void should_redirect_to_deeplink_when_valid_sub()
    {
        //Arrange
        _controllerMock.SetupGet(t => t.TenantId).Returns(guid);
        _controllerMock.SetupGet(t => t.SelectedSubscriptionId).Returns(guid);
        var formValues = new Dictionary<string,string>();
        formValues["wctx"] = "/some/deep/link";
        _controller.SetFakeControllerContext(formValues);

        // Act
        var result = _controller.Index() as ViewResult;

        //// Assert
        Assert.That(result.ViewName, Is.EqualTo(string.Empty));
        Assert.That(result.ViewBag.StartHash, Is.EqualTo("/some/deep/link"));
        //Assert.That(result.RouteValues["action"], Is.EqualTo("Register"));

        _mockRepo.VerifyAll();
    }
}

As you can see, we’re setting up a couple of dependencies, then creating the SUT (_controller) as a partial mock in the setup. In the test, we’re setting up the request value collection and then exercising the SUT to check if we get redirected to a deep link. This works – but the test set up is too complicated. Yes – we need to create a partial mock that and then set up expectations that correspond to a valid user who has a valid subscription – but all this is lost in the details. As such, the test set up is hard to understand and hence hard to trust.

I recently came across this pluralsight course  and there were a few thoughts that hit home right away, namely:

  1. Tests should be DAMP (Descriptive And Meaningful Phrases)
  2. Tests should be easy to review

Test setups require various objects in different configurations – and that’s exactly what a Builder is good at. The icing on the cake is that if we can chain calls to the builder, then we move towards evolving a nice DSL for tests. This goes a long way towards improving test readability – tests have become DAMP.

So here’s what the Builder API looks like from the client (the test case):

[TestFixture]
public class AppControllerTests {
    [SetUp]
    public void Setup()
    {
        _mockRepo = new MockRepository(MockBehavior.Strict);
        _tenantRepoMock = _mockRepo.Create();
        _tenantMapRepoMock = _mockRepo.Create();
        guid = Guid.NewGuid();
    }

    [Test]
    public void should_redirect_to_deeplink_when_valid_sub()
    {
        var formValues = new Dictionary<string, string>();
        formValues["wctx"] = "/some/deep/link";

        var controller = new AppControllerBuilder()
            .WithFakeHttpContext()
            .WithSubscriptionId(guid)
            .WithFormValues(formValues)
            .Build();

        // Act
        var result = _controller.Index() as ViewResult;

        //// Assert
        Assert.That(result.ViewName, Is.EqualTo(string.Empty));
        Assert.That(result.ViewBag.StartHash, Is.EqualTo("/some/deep/link"));
        //Assert.That(result.RouteValues["action"], Is.EqualTo("Register"));

        _mockRepo.VerifyAll();
    }
}

While I knew what to expect, it was still immensely satisfying to see that:

  1. We’ve abstracted away details like setting up mocks, that we’re using a partial mock, that we’re even using MVC mock helper utility behind the AppControllerBuilder leading to simpler code.
  2. The Builder helps readability of the code – its making it easy to understand what preconditions we’d like to be set on the controller. This is important if you’d like to get the test reviewed by someone else.

You might think that this is just sleight of hand – after all, have we not moved all the complexity to the AppControllerBuilder? Also, I haven’t shown the code – so definitely something tricky is going on ;)?

Well not really – the Builder code is straight forward since it does one thing (build AppControllers) and it does that well. It has a few state properties that track different options. And the Build method basically uses the same code as in the first code snippet to build the object.

Was that all? Well not really – you see, as always, the devil’s in the details. The above code is’nt real – its  more pseudo code. Secondly, an example in isolation is easier to tackle. However, IRL (in real life), things are more complicated. We have a controller hierarchy. Writing builders that work with the hierarchy had me wrangling with generics, inheritance and chainability all at once :). I’ll post a follow up covering that.

Advertisements

One thought on “Unit Tests: Simplifying test setup with Builders

  1. Pingback: Mixing Generics, Inheritance and Chaining | Nifty tidbits

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s