Using AutoFixture and Bogus to keep tests expressive
I use AutoFixture heavily in tests. It constructs objects, follows object graphs, creates the system under test, supplies generated values through AutoData, and works with auto-mocking customizations when dependencies need to be faked.
For a long time, that also meant I used AutoFixture for almost all test data generation.
Then I ran into a slightly different problem.
I was testing an importer that processed batches of contacts. I did not need one carefully crafted contact with one carefully chosen edge case. I wanted the batch as a whole to contain useful variation: some optional fields should be missing, some values should depend on other values, and collections should have different sizes.
AutoFixture could create the objects. What I wanted was a better way to describe what those objects should look like.
That is where Bogus was a much better fit. It lets the incidental data vary without making that variation the scenario the test is trying to explain.
That is the split I ended up with: Bogus describes richer specimen data, while AutoFixture composes the surrounding test context.
A batch with useful variation
Imagine an importer receiving this shape:
public class ImportedContactBatch
{
public List<ImportedContact> Contacts { get; init; } = [];
}
public record ImportedContact
{
public string FirstName { get; init; } = default!;
public string LastName { get; init; } = default!;
public string Email { get; init; } = default!;
public string? JobTitle { get; init; }
public Uri? LinkedInUrl { get; init; }
public string? PhoneNumber { get; init; }
public string[] Tags { get; init; } = [];
}
For an importer test, I might want twenty contacts where:
- names look like names;
- emails are derived from first and last name;
- job titles are sometimes missing;
- LinkedIn URLs only exist when a job title exists;
- phone numbers are present only for some contacts;
- tags vary in number.
None of those individual values is the scenario. The scenario is that the importer can process the batch correctly even when optional data varies from one contact to another.
I am not asserting that the generated batch contains any particular combination. The variation is background data, not the behavior under test.
Bogus gives me a direct way to describe that population:
using Bogus.Extensions;
var faker = new Faker<ImportedContact>()
.RuleFor(x => x.FirstName, f => f.Name.FirstName())
.RuleFor(x => x.LastName, f => f.Name.LastName())
.RuleFor(
x => x.Email,
(f, x) => f.Internet.Email(x.FirstName, x.LastName))
.RuleFor(
x => x.JobTitle,
f => f.Name.JobTitle().OrNull(f, 0.35f))
.RuleFor(
x => x.LinkedInUrl,
(f, x) => x.JobTitle is null
? null
: new Uri($"https://www.linkedin.com/in/{f.Internet.UserName(x.FirstName, x.LastName)}"))
.RuleFor(
x => x.PhoneNumber,
f => f.Phone.PhoneNumber().OrNull(f, 0.5f))
.RuleFor(
x => x.Tags,
f => f.Make(
f.Random.Number(0, 4),
() => f.Commerce.Department())
.Distinct()
.ToArray());
var contacts = faker.Generate(20);
The useful part is not just that Bogus produces better-looking strings.
The rules describe relationships and distributions. Email depends on the generated name. LinkedInUrl depends on JobTitle. Optional fields are absent with different probabilities. The result is a batch with different combinations of data without manually enumerating those combinations.
That API matches the problem I am trying to express.
Could AutoFixture do this?
Yes.
AutoFixture is highly extensible. I could write a factory, a customization, or a specimen builder that implements the same generation rules.
For example, a simplified version could look like this. It is intentionally simplified to show the coordination cost, not to reproduce Bogus’s realistic data generators:
fixture.Customize<ImportedContact>(composer => composer
.FromFactory(() =>
{
var firstName = fixture.Create<string>();
var lastName = fixture.Create<string>();
var jobTitle = Random.Shared.NextDouble() < 0.35
? null
: fixture.Create<string>();
return new ImportedContact
{
FirstName = firstName,
LastName = lastName,
Email = $"{firstName}.{lastName}@example.com",
JobTitle = jobTitle,
LinkedInUrl = jobTitle is null
? null
: new Uri($"https://linkedin.example/{firstName}-{lastName}")
};
}));
There is nothing fundamentally impossible here. With enough customization, AutoFixture can generate almost anything I need.
But the shape of the code has changed. I am creating local variables, making random decisions, coordinating dependent values, and constructing the object imperatively. I am writing a little generator.
Bogus already gives me an API for describing those rules.
That distinction matters more to me than a checklist of which library can technically generate which values.
AutoFixture still composes the test
Bogus being better at describing this particular data does not make it a replacement for AutoFixture.
Assume ImportedContact generation has been delegated to the Bogus configuration above. The rest of the test can stay in AutoFixture’s composition model:
[Test]
[AutoData]
public async Task Imports_contacts_without_job_information(
[Frozen] IContactRepository repository,
ContactImporter sut,
ImportedContactBatch batch)
{
batch.Contacts[0] = batch.Contacts[0] with
{
JobTitle = null,
LinkedInUrl = null
};
var result = await sut.Import(batch);
Assert.That(result.ImportedCount, Is.EqualTo(batch.Contacts.Count));
}
The generated batch still contains all the incidental variation described by Bogus. The only thing the test makes explicit is what matters to this scenario: at least one contact has no job information.
AutoFixture is doing much more here than generating strings and objects.
It can construct the SUT and its dependency graph, integrate with a mocking library, freeze selected specimens so the same instance is reused elsewhere, and feed generated values directly into the test method.
Those are composition concerns.
Bogus is better at describing what selected data should look like. AutoFixture is better at integrating that data with everything around it.
Bogus describes the data. AutoFixture composes the test.
Where should the Bogus rules live?
There is no single answer.
If a set of rules describes a sensible default representation of a type across the test suite, putting that configuration in shared fixture infrastructure can make sense.
If the distribution exists because of one workflow, I would keep it close to that workflow instead.
The same principle I use for AutoFixture customizations applies here: shared configuration should describe useful defaults, while scenario-specific differences should remain visible in the test.
That becomes especially important around nullability.
If null is the scenario, make it explicit
The probabilistic nulls in the importer example are useful because no individual null is significant. I simply want optional data to vary naturally across generated contacts, and I do not assert on any particular mix.
That is very different from a test whose behavior specifically depends on a field being absent.
In that case, I would make the missing values explicit:
var contact = fixture.Build<ImportedContact>()
.Without(x => x.JobTitle)
.Without(x => x.LinkedInUrl)
.Create();
Now the test says exactly what makes this contact special.
I do not want a 35% chance of generating the scenario I care about. The null is the scenario, so it belongs in the test setup.
The same applies if a test requires both kinds of contact to be present: create that distinction explicitly, then let Bogus vary the incidental data around it.
Probabilistic generation is useful background variation. It should not hide the behavior being tested. If reproducing a generated batch matters, Bogus can be seeded; I still would not use a seed to encode a scenario that should be explicit in the test.
Combining them is useful, but the boundary leaks
Once the responsibilities are separated, combining the two libraries is an obvious next step.
AutoFixture needs an ImportedContact; Bogus knows how I want an ImportedContact to be populated. Delegating creation of that type is straightforward enough.
The interesting problems start when I expect both libraries to participate deeply in creating the same specimen.
For example, I may want Bogus to generate a few properties while AutoFixture generates the rest. Or I may want a Bogus rule to reuse an instance that AutoFixture has already frozen elsewhere in the test.
Those scenarios expose the fact that the two libraries do not share a specimen context. Bogus does not automatically know what AutoFixture has already generated or frozen, and dividing ownership of individual properties becomes awkward.
That is a useful guardrail: the combination works best when the boundary is clear.
The small integration I put together
I ended up putting together a small AutoFixture extension to make the simple delegation case reusable. The customization tells AutoFixture to let Bogus create an ImportedContact whenever that type is requested, while AutoFixture continues composing the surrounding object graph:
fixture.CustomizeWithBogus<ImportedContact>(faker => faker
.RuleFor(x => x.FirstName, f => f.Name.FirstName())
.RuleFor(x => x.LastName, f => f.Name.LastName())
.RuleFor(
x => x.Email,
(f, x) => f.Internet.Email(x.FirstName, x.LastName)));
If you are interested in how it works, the source is in the AutoFixtureExtensions repository. If you just want to use it, the Bogus integration is published as Kralizek.AutoFixture.Extensions.Bogus.
It works well within the boundary described above: Bogus owns generation of the selected type, and AutoFixture owns the surrounding test context.
It is not a complete shared-generation model. Bogus does not automatically participate in AutoFixture’s frozen specimen context, and selectively mixing ownership of properties is not particularly elegant.
For the cases where the boundary is clear, though, that simple delegation has been enough for me.
Choosing the boundary
I still reach for AutoFixture alone when arbitrary valid specimens are enough and the difficult part is composing the test around them.
I reach for Bogus when the generated data itself needs rules, relationships, or distributions that are easier to describe explicitly.
And I combine them when those richer specimens need to live inside a test context that AutoFixture is already composing.
The important distinction is not which library can technically do more.
AutoFixture can be extended to generate almost anything. Bogus can often describe that data more naturally. Let each tool handle the part of the test it expresses best.
Recap
AutoFixture and Bogus overlap, but they become more useful together once their responsibilities are kept clear. I let AutoFixture assemble the test context and use Bogus where the data itself needs richer rules, relationships, or distributions.
That keeps the test expressive without forcing either library to solve the problem the other already describes better.
Support this blog
If you liked this article, consider supporting this blog by buying me a pizza!