May 12, 2010

Structuring tests using contexts and scenarios

One of the greatest challenges of writing good tests is to keep the tests short and readable. This can be achieved by composition - here is the way I use to structure AAA-style tests:

[Test]
public void ProductValidator_IllegalProductCode_Fails()
{
    // Arrange
    var context = new TestContextBuilder<ProductValidatorContext>()
        .WithScenario(AnonymousProduct)
        .WithScenario(ProductHasIllegalProductCode)
        .BuildContext();
    
    ProductValidator sut = context.CreateSubjectUnderTest();

    // Act
    bool res = sut.Validate();

    // Assert
    Assert.IsFalse(res);
}

The goal is to provide more readable tests and to encapsulate (and reuse) test setup code. As a bonus it will be easy to move between tests and these context/scenario-style tests and using a framework like StoryQ .

Using AAA syntax, the Arrange part of each test uses a specific test context, and aditionally applies one or more scenarios to this context. In the Act part the Subject Under Test is exercised in the context, and in the Assert part the assertions are made, typically on the Subject Under Test or on the context.

The test context:

The test context is the context needed to run a test on the subject under test. In other words, it is the fixed part that stays the same for each test in your test class. You want to keep the context at a minimal level, meaning that the context establishes just enough stuff to be able to create the subject under test. Also the context exposes the things you might want to modify from test to test or make assertions against. We often place the test context class as a private, nested class inside the test class itself.

public class ProductValidatorContext : TestContextBase
{
    public Product TheProduct { get; set; }

    public ProductValidator CreateSubjectUnderTest()
    {
        return new ProductValidator(TheProduct);
    }
}

The scenarios:

Each specific test will use a context builder to build the context. The context builder initializes the context and allows you specify one or more scenarios to apply to the context. Scenarios can be expressed using simple lambda expressíons, methods or classes:

Using lambda:
var context = new TestContextBuilder<ProductValidatorContext>()
    .WithScenario(AnonymousProduct)
    .WithScenario(x => x.TheProduct.Code = "Illegal product code")
    .BuildContext();

Using a method:
var context = new TestContextBuilder<ProductValidatorContext>()
    .WithScenario(AnonymousProduct)
    .WithScenario(ProductHasIllegalProductCode)
    .BuildContext();

...

private void ProductHasIllegalProductCode(ProductValidatorContext context)
{
    context.TheProduct.Code = "Some illegal product code";
}

Using a class:
var context = new TestContextBuilder<ProductValidatorContext>()
    .WithScenario(AnonymousProduct)
    .WithScenari<ProductHasIllegalProductCode>()
    .BuildContext();

...

private class ProductHasIllegalProductCode : TestScenarioBase<ProductValidatorContext>
{
    public override void Apply(ProductValidatorContext context)
    {
        context.TheProduct.Code = "Some illegal product code";
    }
}

Scenario classes can expose properties that are specific to the scenario in case you need to assert against these:

Assert.That(context.Scenario<SomeProductInEditing>.TheProduct.Name, Is.Not.Null);

So typically the context establishes mocks or test instances of all the dependencies of the subject under test. The scenario(s) set the behavior of these mocks or builds the test instances. Here I typically use frameworks like Moq and Autofixture.

I have put the context builder as well as base classes for the contexts and scenarios on Code Gallery.

No comments:

Post a Comment