A unit test can spend more lines constructing irrelevant objects and mocks than expressing the behavior it protects. That Arrange boilerplate slows reviews, obscures the important inputs, and becomes another structure to maintain when constructors change.
This article shows how AutoFixture, AutoData, and AutoNSubstitute can generate data and compose dependencies so a test keeps only the setup that matters. It is aimed at experienced .NET developers and technical leads evaluating whether convention-based fixture generation will improve—or hide—the intent of their test suite.
Why Arrange Boilerplate Hurts Unit Tests
Who writes unit tests at work? Probably almost everyone reading this article.
Who runs short of time while writing them? Probably almost everyone who writes them.
I will assume unit tests need no defense here.
- Building a new feature? Verify the implementation.
- Found a bug? Cover the fix with a test.
- Trying to understand legacy code? Debug it through the test runner.
No matter how many AI tools appear, the situation remains familiar: the sprint has been packed with business features, and your manager looks like this:
The problem is a lack of time. Before we can tackle it, though, we need to talk about…
Arrange, Act, and Assert in .NET Tests
Teams use different libraries and tools, but in my commercial experience, two conventions work well for organizing and structuring test code.
I am talking, of course, about AAA and Roy Osherove's naming convention.
AAA in Plain English
As you may have guessed, this is not about batteries or video games. It is about unit testing.
Programming is already complex enough; navigating a codebase becomes even harder when everything is scattered and nothing is where it belongs.
Life is much easier when everything has a clear place.
Tests can also be organized into clear, concrete stages:
- Arrange. Set up the test: create objects, prepare data, configure mocks, and so on.
- Act. Perform the action—call the functionality under test.
- Assert. Verify the result. Check the data, object state, expected calls, errors, or anything else the scenario requires.
Arrange-Act-Assert gives tests a simple, effective, and recognizable structure.
Roy Osherove in Plain English
A second convention covers test names.
Roy Osherove's test-naming convention looks like this:
UnitOfWork_StateUnderTest_ExpectedBehavior
For English speakers, the structure is mostly self-explanatory. A test name specifies:
- the unit of work,
- the state configured by the test,
- the expected behavior.
Does that look familiar? If it reminded you of AAA, you have the idea.
This simple rule is easy to adopt, yet it improves project structure and navigation.
Example
public record Adder(int Value)
{
public Adder Add(Adder that) =>
new(Value + that.Value);
}
public class AdderTests
{
[Fact]
public void Add_AddingToZero_ResultNotAffectedByZero()
{
// arrange
Adder ten = new(10);
Adder zero = new(0);
// act
var sum = ten.Add(zero);
// assert
sum.Should().Be(ten);
}
}
The Cost of Manual Test-Data Setup
Why did we need to discuss these conventions? Because this kind of structure makes the process systematic—and makes bottlenecks obvious.
Unit tests really have one bottleneck: Arrange. Act and Assert usually take about two lines each. Arrange is where things get interesting—and large. Object setup, collections, integration behavior, configuration: it all lives there.
Now imagine that we are working on a typical task in an enterprise codebase. We write some thoroughly average code and end up with an equally average…
A Typical Handler
public class SomeHandler(
ISomeRepository repository,
ISomeProcessingService service,
ISomeSessionAccessor sessionAccessor,
ISomeExternalDataProvider provider,
IOptions<SomeOptions> options,
ILogger<SomeHandler> logger)
{
private readonly SomeOptions _options = options.Value;
public async Task<SomeResponse> Handle(SomeRequest request, CancellationToken ct)
{
if (request.Field1 < 0)
throw new SomeException();
var entity = await repository.GetByField2(request.Field2, ct);
if (request.Field3)
{
await service.ProcessAsync(entity, ct);
logger.LogInformation("processed");
}
var session = sessionAccessor.Session;
session.ChangeStatus();
var externalData = await Task.WhenAll(
_options.Sources.Select(
source => provider.GetCollectionBySource(source, ct)));
return new SomeResponse(
entity,
externalData.SelectMany(x => x).ToArray());
}
}
This is your average piece of JSON plumbing: it changes something in a session, logs something, reads some configuration, writes something to a database, and returns something to the outside world.
A meaningless and merciless sequence of symbols, even if it is semantically and syntactically valid.
Now let us write a unit test according to London-school practices. It will not even fit on a single PowerPoint slide.
I used NSubstitute for mocking: www.nuget.org/packages/NSubstitute.
I prefer it for its concise API and lack of SponsorLink incidents.
public class SomeHandlerTests
{
[Fact]
public async Task Handle_HappyPath_DoesNotThrow()
{
var repository = Substitute.For<ISomeRepository>();
repository.GetByField2(default, default)
.ReturnsForAnyArgs(new SomeEntity(
Field2: nameof(SomeEntity.Field2),
Data: nameof(SomeEntity.Data)));
var service = Substitute.For<ISomeProcessingService>();
var sessionAccessor = Substitute.For<ISomeSessionAccessor>();
sessionAccessor.Session.Returns(Substitute.For<ISession>());
var provider = Substitute.For<ISomeExternalDataProvider>();
provider.GetCollectionBySource(default, default)
.ReturnsForAnyArgs([
new SomeExternalData(Guid.NewGuid(), Content: 1.ToString(), [1, 2, 3]),
new SomeExternalData(Guid.NewGuid(), Content: 2.ToString(), [4, 5, 6]),
]);
var handler = new SomeHandler(
repository,
service,
sessionAccessor,
provider,
Options.Create(new SomeOptions { Sources = ["source1", "source2"] }),
NullLogger<SomeHandler>.Instance);
var response = await handler.Handle(new SomeRequest(
Field1: 123,
Field2: "123",
Field3: true),
ct: default);
Assert.NotNull(response);
}
}
All that just to test the basic happy path.
And we still have branches, behavior changes, integration failures, and much more. You might extract SUT (system under test) creation into a separate method and call it from every test.
But the application contains other handlers and services. The code will be duplicated, and the boilerplate will grow at least linearly.
What Can We Do?
Arrange has one simple goal: prepare the test.
To achieve it, boilerplate code prepares data and behavior for the scenario we want to run.
The answer practically suggests itself:
- Automate data preparation. Eliminate as many
new()and[]calls as possible. - Automate behavior preparation. Eliminate as much mock creation and setup as possible.
Reducing Arrange with AutoFixture and AutoData
Before discussing how, let us meet the tools whose combination will solve the problem. These are several NuGet packages you may or may not have encountered. The first is…
AutoFixture
www.nuget.org/packages/AutoFixture
This NuGet package can create instances of data types with public constructors:
var fixture = new Fixture();
var bar = fixture.Create<Bar>();
record Foo(Guid Id);
record Bar(
Foo Foo,
string Name,
bool IsBaz,
int Number);
The library was created by Mark Seemann, author of Dependency Injection in .NET.
It solves exactly the problem we identified: when a meaningless 300-field DTO needs to go into the database, you can replace manual initialization with a one-line call.
The code above creates an object like this:
{
"Foo": {
"Id": "537478f7-a6f0-4e85-8ca2-1d7e0da97e7e"
},
"Name": "Name0e4afc29-8ef7-4991-aaa1-2294a456cccc",
"IsBaz": false,
"Number": 58
}
AutoData
www.nuget.org/packages/AutoFixture.Xunit2/5.0.0-preview0011
I am lazy not only as a programmer: I do not want to instantiate Fixture objects and create every related object myself. I want everything ready by the time the test starts.
Fortunately, AutoFixture has connectors for test frameworks such as xUnit. Informally, I call this family of features AutoData, after the attribute that provides the behavior I need.
We apply the attribute to theory tests. It implicitly creates a data source whose Fixture instance produces the objects requested by the theory parameters.
public class TestClass
{
[Theory, AutoData]
public void TestMethod1(string foo)
{
foo.Should().NotBeEmpty();
}
[Theory, AutoData]
public void TestMethod2(Foo foo)
{
foo.Bar.Should().NotBeEmpty();
}
}
public class Foo
{
public string Bar { get; set; }
}
Strictly speaking, this is a synthetic theory because it has only one case. But what if you already have data sources?
There are InlineAutoData, MemberAutoData, ClassAutoData, and other attributes:
public class TestClass
{
[Theory]
[MemberAutoData(nameof(TestData))]
public void TestMethod3(int a, int b, int c)
{
c.Should().BeGreaterThan(a + b);
}
public static IEnumerable<object[]> TestData =
[
[-1, -2],
[-3, -4]
];
}
Here, the data source supplied the first two test parameters for each case, while AutoFixture created the third. One theory now covers every listed case without repeating incidental setup.
Data preparation is now automated, so we are ready to move on.
AutoData + Mocks = AutoNSubstitute
What should we do about mocks?
When preparing them, we create a mock instance and configure some return value—either an object or another mock.
What if we asked AutoFixture to create a mock whose return values were populated with the results of Create calls?
It turns out that the AutoNSubstitute customization does exactly that.
NuGet packages also exist for Moq, RhinoMocks, and FakeItEasy.
To create mocks, enhance the data generator with Customize. The test looks like this:
public class TestClassClass
{
[Fact]
public async Task Test()
{
var fixture = new Fixture()
.Customize(
new AutoNSubstituteCustomization
{
ConfigureMembers = true
});
var provider = fixture.Create<ISomeExternalDataProvider>();
var collection = await provider.GetCollectionBySource(
source: fixture.Create<string>(),
ct: default);
Assert.NotEmpty(collection);
}
}
The debugger screenshot shows the service instance that AutoFixture created for ISomeExternalDataProvider, including the automatically mocked method's return value.
Combining AutoFixture, AutoData, and AutoNSubstitute
We can now combine all three pieces behind a single attribute.
A test method marked with this attribute receives both objects and configured mocks:
public class AutoNSubstituteDataAttribute() :
AutoDataAttribute(
() => new Fixture().Customize(
new AutoNSubstituteCustomization
{
ConfigureMembers = true
}));
This attribute is not included out of the box. Mark Seemann suggests creating it yourself in his blog, using inheritance as the extension model.
Return to our typical handler. The test that would not fit on any screen can now be rewritten in two lines, letting us ignore the routine setup and focus on scenarios that verify the logic:
public class SomeHandlerTests2
{
[Theory, AutoNSubstituteData]
public async Task Handle_HappyPath_DoesNotThrow(
SomeRequest request,
SomeHandler handler)
{
var response = await handler.Handle(request, ct: default);
Assert.NotNull(response);
}
[Theory, AutoNSubstituteData]
public async Task Handle_Field1LessThanZero_ThrowsSomeException(
SomeRequest request,
SomeHandler handler)
{
await Assert.ThrowsAsync<SomeException>(
() => handler.Handle(request with { Field1 = -1 }, ct: default));
}
[Theory, AutoNSubstituteData]
public async Task Handle_NoExternalData_ItIsEmptyInResponse(
SomeRequest request,
[Frozen] ISomeExternalDataProvider provider,
SomeHandler handler)
{
provider.GetCollectionBySource(default, default)
.ReturnsForAnyArgs([]);
var response = await handler.Handle(request, ct: default);
response.ExternalDataCollection.Should().BeEmpty();
}
}
Advanced Scenarios
AutoFixture is a powerful data generator, but it cannot infer every domain rule or create exactly the data you want. You have to tell it how.
That brings us back to extending its behavior. Inheritance is not an ideal extension model here: one codebase may contain multiple domains and business-logic implementations, potentially causing an explosion of derived attributes that quickly become confusing.
We can modify the behavior another way. Here is a real example from practice. Suppose we have a Value Object that models a discrete rating from 1 to 5:
public struct Rate
{
public Rate(byte value)
{
if (value is >= 1 and <= 5)
Value = value;
else
throw new ArgumentOutOfRangeException(
nameof(value),
value,
message: "rate can be from 1 to 5");
}
public byte Value { get; }
}
If we ask for a rating object in a test, we get an error:
public class TestClassClass
{
[Theory, AutoData]
public void Test22(Rate rate)
{
rate.Value.Should().NotBe(0);
}
}
The reason is that AutoFixture generates data according to its own rules:
Generic Attributes + static abstract
Let us investigate. Look inside [AutoData] and you will see a constructor that accepts a Func<Fixture> delegate:
public AutoDataAttribute()
: this(() => new Fixture())
{
}
We can use that hook to configure the Fixture object however we need. But how do we obtain different services and dependencies from a static constructor context, especially when attribute parameters can contain only constants and types?
Two C# 11 features help us: static abstract members and generic attributes. They work surprisingly well together.
public interface IFixtureCustomizer
{
static abstract void Customize(IFixture fixture);
}
public class AutoDataAttribute<TFixtureCustomizer> : AutoDataAttribute
where TFixtureCustomizer : IFixtureCustomizer
{
public AutoDataAttribute() : base(
fixtureFactory: () =>
{
var fixture = new Fixture();
TFixtureCustomizer.Customize(fixture);
return fixture;
})
{
}
}
Our data-generator extension for ratings now looks like this:
public class RateGenerator : IFixtureCustomizer
{
public static void Customize(IFixture fixture) =>
fixture.Register(() => new Rate((byte)Random.Shared.Next(1, 6)));
}
public class TestClassClass
{
[Theory, AutoData<RateGenerator>]
public void Test22(Rate rate)
{
rate.Value.Should().NotBe(0);
}
}
If your project is still on .NET 6 or earlier, adapt the solution as follows:
- Make
IFixtureCustomizerinstance-based. - Have the attribute extension accept a type supplied through
typeof. - Instantiate the customizer inside the attribute through
Activator.CreateInstance.
Test Data Types
You can also describe the desired behavior and test-data generation scenario with classes whose objects AutoData creates as theory parameters.
For example, AutoFixture uses the Fare library to generate strings from a regular expression.
I recently needed this in a pet project: a test had to generate random sequences of strings from a particular lexical grammar. Armed with the documentation and some ingenuity, I automated preparation of those strings with a class passed as a test parameter:
public record LexerInput([property: MinLength(10), MaxLength(25)] TokenInput[] TokenInputs) : IReadOnlyList<string>
{
public IEnumerator<string> GetEnumerator() =>
TokenInputs.Select(x => x.Value).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public int Count => TokenInputs.Length;
public string this[int index] => TokenInputs[index].Value;
public override string ToString() =>
TokenInputs.Aggregate(
TokenInput.AdditiveIdentity,
(x, y) => x + y).Value;
}
public record TokenInput([property: RegularExpression(TokenInput.Pattern)] string Value) :
IAdditiveIdentity<TokenInput, TokenInput>,
IAdditionOperators<TokenInput, TokenInput, TokenInput>
{
[StringSyntax(StringSyntaxAttribute.Regex)]
public const string Pattern = "[a-zA-Z]+|[0-9]+|[+]{2}";
public static TokenInput operator +(TokenInput left, TokenInput right) =>
new(left.Value + " " + right.Value);
public static TokenInput AdditiveIdentity { get; } = new(string.Empty);
}
AutoFixture reads data-creation rules from attributes in the System.ComponentModel.DataAnnotations namespace.
Here, [RegularExpression] supplies the pattern used to initialize the annotated string property, while [MinLength] and [MaxLength] provide the bounds for choosing a random array length.
The test uses it like this:
public class RegexLexerTests(ITestOutputHelper output)
{
[Theory, AutoHydraScriptData]
public void GetTokens_MockedRegex_ValidOutput(
LexerInput input,
[Frozen] IStructure structure,
RegexLexer lexer)
{
output.WriteLine(input.ToString());
var patterns = TokenInput.Pattern.Split('|');
structure.Regex.ReturnsForAnyArgs(
new Regex(string.Join('|', patterns.Select((x, i) => $"(?<TYPE{i}>{x})"))));
var tokenTypes = Enumerable.Range(0, patterns.Length)
.Select(x => new TokenType($"TYPE{x}"))
.ToList();
// ReSharper disable once GenericEnumeratorNotDisposed
structure.GetEnumerator()
.ReturnsForAnyArgs(_ => tokenTypes.GetEnumerator());
var tokens = lexer.GetTokens(input.ToString());
for (var i = 0; i < input.Count; i++)
{
output.WriteLine(tokens[i].ToString());
tokens[i].Value.Should().BeEquivalentTo(input[i]);
tokens[i].Type.Should().BeOneOf(tokenTypes);
}
}
}
Takeaways
AutoFixture can automate both parts of Arrange:
- Data preparation = AutoFixture + AutoData.
- Behavior preparation = AutoFixture + AutoData + mocks.
The technique works best when generated values are incidental to the behavior under test. Keep meaningful boundary values explicit, freeze dependencies whose identity matters, and add fixture customizations only when they encode a stable project-wide convention. If a reader must reverse-engineer the fixture to understand the scenario, the abstraction has removed too much Arrange.
Modern C# features let customizations remain type-safe even as the fixture setup becomes reusable across a large test suite.
Related .NET Content
Follow StepOne on GitHub for open-source C#/.NET projects, compiler experiments, production-focused examples, and new releases.







Top comments (0)