Tuesday, October 23, 2007

Constructor dependency injection in a test framework!

Nonsense?  Hardly!  (Translation: I'm doing it already and it's very useful.)

After reading the plans for xUnit.Net beta 2 that involve a cumbersome IUseFixture<T> interface type, I decided to pass on some of my ideas for MbUnit v3 in hopes that they will be of value to others.

Defining the problem

Problem: I've got some common state or behavior that I want to share across multiple tests within a fixture (or perhaps across multiple fixtures).

Answers:

  • With MbUnit v2 or NUnit, just put the state on your fixture class and use [TestFixtureSetUp] and [TestFixtureTearDown].
  • With MbUnit v3, you can do that or you can create "mixins" that are defined using dependency injection on the fixture class and are first-class participants in the test suite.  Mixins are standalone classes that augment fixtures with custom behavior via attributes like [TestFixtureSetUp] or [SetUp] as usual but they can also contribute new tests, data sets, and other goodies.  Using a mixin is as simple as declaring a property, field, or constructor parameter of the mixin type on your fixture (or on another mixin).  Look for this feature in MbUnit v3 - Alpha 2 in a month or two.
  • With xUnit beta 2, your test class should implement IUseFixture<T> and put the necessary logic in T's constructor and Dispose.  But there are drawbacks to this approach.  One of them is that it results in additional boilerplate for providing setter methods to inject the shared instance.  Another is that the setter methods are obviously called after the constructor runs so you can't consume the shared instance as part of any constructor-based initialization.
  • Something else?

Constructor dependency injection to the rescue!

Another way to think of the problem is to imagine we are trying to inject services into the test fixture.  The service initialization and disposal can be taken care of by the framework per-test or per-fixture as required.  Let's see what constructor dependency injection can do here...

// Just an ordinary test fixture...
[TestFixture]
public class SomeTests
{
    private readonly IE ie;
    private readonly FixtureGlue glue;

    // The framework can use constructor dependency injection
    // to provide instances of a WatiN IE browser wrapper
    // and some custom FixtureGlue type as needed since they
    // are concrete types.  They also both happen to implement
    // IDisposable so that the framework can take care of
    // disposing them too.
    //
    // So to inject concrete components like this into a test,
    // all we really need is to declare a constructor with the
    // right signature.
    public SomeTests(IE ie, FixtureGlue glue)
    {
        this.ie = ie;
        this.glue = glue;
    }

    // Some ordinary test...
    [Test]
    public void MyTest()
    {
        ie.GoTo("http://www.google.com");
        ie.TextField("q").TypeText("MbUnit");
        ie.Button("iBtn").Click();

        Assert.AreEqual("http://www.mbunit.com/", ie.Url);
    }
}

// Encapsulates some common behavior we want to run before
// and after all tests.
public class FixtureGlue : IDisposable
{
    public FixtureGlue()
    {
        // configure something in the environment
    }

    public void Dispose()
    {
        // tear down whatever we did
    }
}

However, we haven't said anything about when we get new instances of IE and FixtureGlue.

Fine-tuning the semantics

Should we create new instances of the IE and FixtureGlue types for each test or share them across all tests within the fixture?

In MbUnit, the expected answer would be probably to share these services because the fixture instance is reused all tests so its constructor will only run once (although in principle we could make this configurable).

In xUnit.Net, the situation is a little different.  xUnit.Net always constructs a new instance for each test.  So by default the test author will probably assume that the injected services are not reused.  However, that's precisely what we want...

To facilitate local reasoning about the tests, we can offer a custom attribute that is applied to the constructor's parameters to explicity state what's going on.  For xUnit.Net, we can add a [Fixture] attribute that states that the value of the associated parameter should be reused for each test in the fixture.  This usage of the word "fixture" seems pretty consistent with xUnit.Net nomenclature.

So our revised constructor declaration looks like this:

public class SomeTests
{
    // Initialized with a new instance of IE that is created
    // and disposed for each test and a shared instance of
    // FixtureGlue that is reused across all tests in the
    // fixture and disposed when finished.
    //
    // So we can see that by default services are recreated
    // each for each test unless the [Fixture] attribute appears.
    public SomeTests(IE ie, [Fixture] FixtureGlue glue)
    {
        // as before...
    }

    // as before...
}

Obviously this idea can be taken much further.  Dependency injection can be used for parameterizing tests in plenty of other interesting ways and with more or less customization of the underlying process...

Spoiler: MbUnit v3 does all sorts of nifty stuff with dependency injection, particularly related to data-driven testing and to increasing the reusability of common test fixture extensions well beyond what can be achieved by subclassing.  Dependency injection has already proven to be immensely beneficial to me in the form of custom MbUnit v2 extensions for data-driven testing.  The plan is to go much much further.

kick it on DotNetKicks.com

2 comments:

Steven Mitcham said...

I'm currently using MbUnit v2 at my company, and I'm eagerly watching the development of v3 and xUnit.net.

I've also just skimmed through the xUnit Test Design Patterns book and the criticism of NUnit and therefore MbUnit with the [TestFixutreSetup] is exactly that the shared state is a 'bad thing' for constructing isolated test environments on a per-test basis.

Since this injection is basically shared state, how do you answer that? I'm guessing that their approach is to eliminate the need of having to remember how to clean up the environment to be ready for the next test, and that has been a concern in many of our test areas.

Taumuon said...

I've been looking at data-driven testing using NUnit for a while, and have created an extension which I've blogged about here: http://taumuon-jabuka.blogspot.com/2008/02/this-is-blog-post-to-explain-exactly.html, and it lives on taumuon.co.uk/rakija.

I'm not that happy with the interface though, can you explain a bit more about the dependency injection in MBUnit? It will either give me more food for thought, or pursuade me to switch to MBUnit to solve some of the issues I was blogging about :)

Thanks!