Exploring Custom Attributes in AutoFixture and xUnit
Writing setup code for unit tests can be a cumbersome activity. Especially in legacy codebases where many tests may be missing, or unreliable, or when the unit being tested is larger than it would be given sufficient time and resources to refactor it apart. When tackling poor-quality software, one good first step can be to solidify the behaviour with tests, to open the door to refactoring with confidence. In this scenario, it is crucial the maintenance of those tests does not cost as much or more than that of the original code.
AutoFixture is an excellent library for obliterating almost all of the typical setup code when writing unit tests. A colleague, who seemed to enjoy writing unit tests a little too much, introduced me to it. It did not take long for the team to begin converting every legacy test the team touched to using it.
This article will not go into general usage of AutoFixture, Mark Seemen and others have covered it comprehensively. What this article aims to provide is a way to combine the declarative SUT factory approach e.g.
[Theory, AutoData]
public void IntroductoryTest(int expectedNumber, MyClass sut)
{
int result = sut.Echo(expectedNumber);
Assert.Equal(expectedNumber, result);
}
With some of the shared setup benefits and convenience that we get from the basic usage.
public class BasicUsageTest
{
private readonly IFixture _fixture = FixtureFactory.Create();
public BasicUsageTest()
{
_fixture.Customize<int>(c => c.FromFactory(() => 9));
}
[Fact]
public void Create__UsesSetup()
{
_fixture.Create<int>().ShouldBe(9);
_fixture.Create<int>().ShouldBe(9);
_fixture.Create<int>().ShouldBe(9);
}
}
You could use custom attributes with a variety of customizations, although it feels like a lot of overhead when all we want is some shared fixture setup for a particular test class.
Instead, taking inspiration from NUnit's SetUp attribute we can define a few new attributes to serve a similar purpose for setting up the fixture.
We begin with a SetupFixtureAttribute
to mark the setup method we want to define within our test classes. There is a little extra fluff in here to silence some warnings that we are on top of. We use the MeansImplicitUse
attribute, which comes from JetBrains.Annotations, since the setup methods will be called later via reflection. We also handle xUnit1013 as suggested by xUnit's documentation.
[MeansImplicitUse]
[AttributeUsage(AttributeTargets.Method)]
[IgnoreXunitAnalyzersRule1013]
public class SetupFixtureAttribute : Attribute { }
// https://xunit.net/xunit.analyzers/rules/xUnit1013
public sealed class IgnoreXunitAnalyzersRule1013Attribute : Attribute
{
}
Unfortunately, we cannot easily extend the built-in AutoDataAttribute
from AutoFixture for this particular use case so we need to replace it with our own. It is a little large to embed nicely into this article, so instead find the SetupAutoDataAttribute
linked, with examples, here. The public interface is kept the same as the AutoDataAttribute
so you can easily add additional attributes with whatever customizations are desired.
Then an attribute can be declared as it would have with AutoFixture's AutoDataAttribute, only now with the SetupAutoDataAttribute
we have defined.
public class AutoDomainDataAttribute : SetupAutoDataAttribute
{
public AutoDomainDataAttribute() : base(FixtureFactory.Create)
{
}
}
With FixtureFactory.Create
, a static class with any default fixture setup necessary between tests.
Finally, we can bring all of our new attributes together in a test class.
public class AutoDomainDataAttributeTest
{
[SetupFixture]
public static void SetupFixture(IFixture fixture)
{
fixture.Customize<int>(c => c.FromFactory(() => 2));
}
[Theory]
[AutoDomainData]
public void SetupFixture__AppliesCustomizations(int a, int b, int c)
{
a.ShouldBe(2);
b.ShouldBe(2);
c.ShouldBe(2);
}
}
With this arrangement, we get the elegance of the AutoData
attribute with the power of the basic usage even for complex setup code. When combined with AutoFixture's existing integration points we have the flexibility of:
Customizations on parameters by deriving from
CustomAttribute
.Customizations on individual tests by deriving from
SetupAutoDataAttribute
.Customizations on tests in a class by using the
SetupFixture
attribute in test classes.Global customizations by using a static fixture factory.
public class CompleteCustomizationsTest
{
[SetupFixture]
public static void SetupFixture(IFixture fixture)
{
fixture.Customize(new MyLongCustomization());
}
/// <summary>
/// 1. Double customization from custom parameter attribute.
/// 2. Int customization from custom SetupAutoDataAttribute.
/// 3. Long customization from SetupFixture marked method.
/// 4. String customization from static fixture factory.
/// </summary>
[Theory]
[IntCustomizedAutoDomainData]
public void Test([DoubleCustomize] double myDouble, int myInt, long myLong, string myString)
{
myDouble.ShouldBe(1.1);
myInt.ShouldBe(100);
myLong.ShouldBe(200);
myString.ShouldBe("bananas");
}
}
That is a tonne of flexibility. You can find complete working examples along with all the attributes described in this article on GitHub. There is also the inline data equivelant for conventional xUnit parameterised tests.
Writing highly testable code reduces the need for such complex setup scenarios but when you do face them, this arrangement can minimize the friction. If you have any comments or suggestions I encourage you to leave a comment below. I am interested in other approaches that are more harmonious with AutoFixture and xUnit's existing patterns.
Subscribe to my newsletter
Read articles from Michael Nelson directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by