Leveraging generic attributes to get test-specific test data

5 minute read

In the realm of unit testing, one of the key challenges is crafting tests that are both robust and maintainable. An essential aspect of this process is the generation and management of test data. Traditionally, test data generation has been a repetitive task, often leading to cluttered and hard-to-maintain test suites. This is where AutoFixture, a powerful .NET library, comes into play, particularly with its seamless integration with popular test frameworks like NUnit and XUnit.

However, even with AutoFixture’s capabilities, the customization of test data for different scenarios can lead to a proliferation of redundant code. In this post, we’re going to tackle this issue head-on. With the advent of C# 11, we’ve been given a powerful new tool: generic attributes. These allow for a more streamlined and efficient approach to customizing test data, reducing redundancy and enhancing the clarity of our test code.

We’ll explore how to effectively use generic attributes in conjunction with AutoFixture to create test-specific customizations. This approach not only simplifies the setup of our tests but also increases their readability and maintainability. Whether you’re a seasoned developer or just starting out with unit testing in .NET, this post will provide valuable insights into using the latest C# features to improve your testing practices.

Feeding data to tests

This integration allows me to write tests like the following one

[Test]
[AutoData]
public void A_test(string value)
{
    Assert.That(value, Is.Not.Null);
}

In the snippet above, the AutoDataAttribute hooks into the NUnit pipeline to create a string with a random value and feed it to the test.

This allows us to write tests that are not bound to well-known values while focusing on the important parts of the test.

Customizing how data is created

The next step is customizing how the AutoDataAttribute generates the values.

We can do this by creating a subclass of the attribute.

In the example below, I install the glue library to integrate FakeItEasy so that AutoFixture can use it to serve fakes when there is no straight forward way to create instances of a type.

In the example below I leverage the FakeItEasy library to setup fake instances of types that are otherwise hard to create.

[AttributeUsage(AttributeTargets.Method)]
public class MyAutoDataAttribute : AutoDataAttribute
{
    public MyAutoDataAttribute() : base(CreateFixture)
    {
    }

    private static IFixture CreateFixture()
    {
        var fixture = new Fixture();
        
        fixture.Customize(new AutoFakeItEasyCustomization
        {
            ConfigureMembers = true,
            GenerateDelegates = true
        });

        return fixture;
    }
}

The problem with this approach is that, when multiple tests need different customizations of the same type, you can only solve it by creating multiple copies of the attribute. Each with its own set of customizations. It works, and with some work you can reduce duplication of code up to a certain point, but eventually you get to a point with tens of attributes.

File local types help reducing the Intellisense clutter, but we still end up writing the same code over and over. To tackle this, I even created a snippet to speed it up.

Welcome generic attributes

Generic attributes are a feature of C# 11 that allows the creation of attributes that accept a closed type as parameter.

Before C# 11, attributes could only accept instances of the class Type.

public class SomeAttribute : Attribute
{
    public SomeAttribute(Type type)
    {
        Type = type;
    }

    public Type Type { get; }
}

[SomeAttribute(typeof(string))]
public string Method() => "something";

With C# 11, we can now create generic attributes.

public class SomeAttribute<T> : Attribute
{
    public Type Type => typeof(T);
}

[SomeAttribute<string>]
public string Method() => "something";

Most importantly, we can use generic constraints to force the compiler to only accept types matching our requirements.

Accepting customizations via attribute

After this brief introduction about generic attributes, we have all we need to inject specific customizations into our AutoDataAttribute.

Let’s first define a IFixtureConfigurator interface.

public interface IFixtureConfigurator
{
    void ConfigureFixture(IFixture fixture);
}

and let’s move the CreateFixture method to a static class for easier reuse. Also, we augment it to accept a list of configurators and use them.

internal static class FixtureHelpers
{
    public static IFixture CreateFixture(params IFixtureConfigurator[] configurators)
    {
        var fixture = new Fixture();

        fixture.Customize(new AutoFakeItEasyCustomization
        {
            ConfigureMembers = true,
            GenerateDelegates = true
        });

        foreach (var configurator in configurators)
        {
            configurator.ConfigureFixture(fixture);
        }

        return fixture;
    }
}

[AttributeUsage(AttributeTargets.Method)]
public class MyAutoDataAttribute : AutoDataAttribute
{
    public MyAutoDataAttribute() : base(() => FixtureHelpers.CreateFixture())
    {
    }
}

Finally, let’s introduce a generic variant of MyAutoDataAttribute that accepts implementations of IFixtureConfigurator that have a parameterless constructor.

[AttributeUsage(AttributeTargets.Method)]
public class MyAutoDataAttribute<T> : AutoDataAttribute
    where T : IFixtureConfigurator, new()
{
    public MyAutoDataAttribute() : base(() => FixtureHelpers.CreateFixture(new T()))
    {
    }
}

As you can see, now we pass a fresh instance of T to the FixtureHelpers.CreateFixture method.

A customized test

Now that we have a version of MyAutoDataAttribute that can be customized via instances of IFixtureConfigurator, let’s put it to good use!

First, we need an implementation of IFixtureConfigurator with the customizations specific to our unit test. We can mark this class as a local type to scope its visibility to the current file.

file class ATestFixtureConfigurator : IFixtureConfigurator
{
    public void ConfigureFixture(IFixture fixture)
    {
        fixture.Inject("my custom string");
    }
}

Now, we can decorate our test so that it injects the ATestFixtureConfigurator into the Fixture configuration pipeline.

[Test]
[MyAutoData<ATestFixtureConfigurator>]
public void A_test(string value)
{
   Assert.That(value, Is.EqualTo("my custom string"));
}

[Test]
[MyAutoData]
public void Another_test(string value)
{
   Assert.That(value, Is.Not.EqualTo("my custom string"));
}

In the snippet above, we can see two unit tests.

The first one has its value generator customized using the ATestFixtureConfigurator so that the incoming string is exactly the value we specified.

The second test uses the base version of MyAutoDataAttribute and it simply receives a randomly generated string.

Present and future

The solution presented in this post still requires extra classes to be created but at least we can be spared from creating multiple copies of the AutoDataAttribute subclass.

Ideally, I would love to feed the configuration as a delegate to the attribute.

[Test]
[MyAutoData(fixture => {
    fixture.Inject("my custom string");
})]
public void Another_test(string value)
{
   Assert.That(value, Is.EqualTo("my custom string"));
}

Unfortunately, this might not come in a long time since it’s not just a problem of the C# compiler, rather of the underlying .NET runtime.

There is already a proposal for allowing lambdas as arguments to attributes, but I don’t see it going anywhere anytime soon.

Recap

In this post, we explored the use of C# 11’s generic attributes to streamline the process of customizing test data in unit testing. We delved into integrating AutoFixture with NUnit and XUnit, demonstrating how to feed tests with dynamic data and customize data creation.

The post highlighted the challenges of managing multiple test customizations and presented a solution through the introduction of generic attributes. By leveraging generic attributes, we showcased a more efficient way to handle test-specific customizations without cluttering the code with numerous attribute classes. This approach not only simplifies the testing process but also enhances the flexibility and reusability of test configurations, paving the way for more refined and maintainable unit tests.

Support this blog

If you liked this article, consider supporting this blog by buying me a pizza!