Most applications need more test data than a few hand-written objects can provide. A single unit test may need one customer, but an integration test often needs hundreds of customers, orders, timestamps, statuses, and relationships that look plausible together.
Bogus is a .NET library for generating that kind of fake data. You describe how an object should be created, and Bogus generates one object or a collection of objects from those rules.
The useful distinction is this: Bogus generates data. It does not replace mocks, assertions, contract tests, or production data-quality checks. It is a tool for creating controlled input for development and testing.
What Bogus does
Bogus provides two things:
Faker<T>describes how to build instances of a type.- Data sets such as
Name,Address,Internet,Commerce,Date, andRandomprovide reusable generators.
The main API is fluent. A rule maps a property to either a fixed value or a callback that receives a Bogus faker instance:
var faker = new Faker<Customer>()
.RuleFor(customer => customer.FirstName, f => f.Name.FirstName())
.RuleFor(customer => customer.LastName, f => f.Name.LastName());
Customer customer = faker.Generate();
List<Customer> customers = faker.Generate(100);
Generate() creates one object. Generate(count) creates a collection. Each call evaluates the rules again, so the generated values normally differ.
Install Bogus
At the time of writing, NuGet lists version 35.6.5; pin the version in real projects when repeatability matters, and update it deliberately when upgrading dependencies.
dotnet add package Bogus --version 35.6.5
The package is usually most useful in test projects, development-only seeders, demo applications, and tools that need a large amount of sample data. Keep it out of production application paths unless generating sample data is an explicit product feature.
Start with a simple faker
Here is a small model for the examples in this post:
public enum CustomerStatus
{
Trial,
Active,
Suspended
}
public sealed class Customer
{
public int Id { get; set; }
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public string Email { get; set; } = "";
public string CountryCode { get; set; } = "";
public CustomerStatus Status { get; set; }
public DateTime CreatedAtUtc { get; set; }
}
Now define the data rules in one place:
using Bogus;
var nextCustomerId = 1;
var customerFaker = new Faker<Customer>("en")
.StrictMode(true)
.RuleFor(customer => customer.Id, _ => nextCustomerId++)
.RuleFor(customer => customer.FirstName, f => f.Name.FirstName())
.RuleFor(customer => customer.LastName, f => f.Name.LastName())
.RuleFor(customer => customer.Email, f => f.Internet.ExampleEmail())
.RuleFor(customer => customer.CountryCode, f => f.Address.CountryCode())
.RuleFor(customer => customer.Status, f => f.PickRandom<CustomerStatus>())
.RuleFor(customer => customer.CreatedAtUtc,
f => f.Date.PastOffset(2).UtcDateTime);
Customer customer = customerFaker.Generate();
List<Customer> customers = customerFaker.Generate(100);
The counter makes this faker stateful. Avoid sharing stateful faker instances across parallel tests. Prefer creating a fresh faker per test or per fixture operation when rules close over counters such as nextCustomerId.
The f parameter is a Faker instance. It exposes the built-in data sets and randomization helpers. For example:
f.Name.FirstName()
f.Address.CountryCode()
f.Date.PastOffset(2)
f.Random.Int(1, 10)
f.PickRandom<CustomerStatus>()
StrictMode(true) is valuable for reusable test fixtures. It makes Bogus complain when a writable property has no rule, so adding a property to the model does not silently leave the fixture incomplete. You can omit it for quick prototypes, but explicit rules make larger fixtures easier to review.
Rules can depend on the object being generated
Some values should be related to other values on the same object. Use the overload that receives both the faker and the object under construction:
var customerFaker = new Faker<Customer>("en")
.RuleFor(customer => customer.FirstName, f => f.Name.FirstName())
.RuleFor(customer => customer.LastName, f => f.Name.LastName())
.RuleFor(customer => customer.Email,
(f, customer) => f.Internet.Email(
customer.FirstName,
customer.LastName));
This produces an email address that matches the generated name. For test systems that must never accidentally send mail, ExampleEmail() is a safer default because it uses a reserved example domain. If the system under test needs a particular email format, prefer a reserved non-resolving domain such as test.invalid and assert that the application treats it as test data.
You can also use a custom callback for domain-specific rules:
public sealed class Subscription
{
public string Plan { get; set; } = "";
public int Seats { get; set; }
}
var planNames = new[] { "Starter", "Team", "Enterprise" };
var subscriptionFaker = new Faker<Subscription>()
.RuleFor(subscription => subscription.Plan,
f => f.PickRandom(planNames))
.RuleFor(subscription => subscription.Seats,
(f, subscription) => subscription.Plan switch
{
"Starter" => f.Random.Int(1, 3),
"Team" => f.Random.Int(4, 25),
_ => f.Random.Int(26, 500)
});
The important part is that the generated values follow the rules your application cares about. A random number between 1 and 500 is not automatically realistic for every plan.
Compose related objects
Real test scenarios rarely contain isolated objects. A customer may have orders, and an order should refer to an existing customer rather than a random integer that happens to look like an ID.
Generate the parent objects first, then use them when defining the child faker:
public sealed class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public decimal Total { get; set; }
public DateTime CreatedAtUtc { get; set; }
}
var customers = customerFaker.Generate(25);
var nextOrderId = 1;
var orderFaker = new Faker<Order>()
.StrictMode(true)
.RuleFor(order => order.Id, _ => nextOrderId++)
.RuleFor(order => order.CustomerId,
f => f.PickRandom(customers).Id)
.RuleFor(order => order.Total,
f => f.Finance.Amount(10, 2_000))
.RuleFor(order => order.CreatedAtUtc,
f => f.Date.PastOffset(2).UtcDateTime);
var orders = orderFaker.Generate(100);
This pattern is often more useful than generating a large object graph in a single expression. Each faker has one responsibility, and the relationship between the generated collections is visible in the test setup.
For a development database, the same approach can be used in a dedicated seeding step:
var customers = customerFaker.Generate(1_000);
var orders = orderFaker.Generate(5_000);
db.Customers.AddRange(customers);
db.Orders.AddRange(orders);
await db.SaveChangesAsync();
Keep this code separate from production startup. A development seeder should be deliberate, repeatable, and impossible to run against a production connection by accident.
If the database owns identity values, let EF Core generate the keys or build child records after the parent keys are known. Explicit integer IDs can conflict with identity and primary-key configuration, depending on how the model is mapped.
Make generated data repeatable
Random data is useful for finding unexpected cases, but a failing test is much easier to investigate when the same input can be generated again. Bogus supports repeatable output through a randomizer seed:
using Bogus;
Randomizer.Seed = new Random(12345);
var customers = customerFaker.Generate(10);
Use a seed when you need to reproduce a failing scenario or keep a development dataset stable while debugging. Do not make tests depend on exact generated names or on the exact order of every random value. Test the properties that matter:
For unit tests, prefer local seeding with UseSeed when you want a specific generated object to be repeatable without coupling other tests to a global random sequence:
var customer = customerFaker
.UseSeed(12345)
.Generate();
Local seeding controls Bogus randomness, not external state captured by your rules. If a faker closes over counters such as nextCustomerId, create a fresh faker before generating the seeded object.
var customer = customerFaker.Generate();
Assert.NotEqual(0, customer.Id);
Assert.False(string.IsNullOrWhiteSpace(customer.Email));
Assert.Matches("^[A-Z]{2}$", customer.CountryCode);
Randomizer.Seed is global state. Set it deliberately, preferably at the boundary of a test or data-generation operation, and be aware that other tests using Bogus may be affected. Also remember that values generated outside Bogus, such as Guid.NewGuid() or DateTime.UtcNow, are not made deterministic by the Bogus seed.
Even with a seed, generated values can change when you upgrade Bogus or its locale data. Avoid asserting exact fake names unless that exact value is the behavior under test. For maximum unit-test stability, stay within the same major version and treat dependency upgrades as a reason to review deterministic fixtures.
For fully stable timestamp fixtures, use a fixed date range or inject a known reference time instead of relying on a Past value relative to now.
Use custom constructors when necessary
Some domain types cannot be created with a parameterless constructor. CustomInstantiator lets the faker create the object using the constructor you need:
public sealed class Account
{
public Account(Guid id)
{
Id = id;
}
public Guid Id { get; }
public string Name { get; set; } = "";
}
var accountFaker = new Faker<Account>()
.CustomInstantiator(f => new Account(f.Random.Guid()))
.RuleFor(account => account.Name, f => f.Company.CompanyName());
var account = accountFaker.Generate();
This keeps the fixture aligned with the production construction rules instead of weakening the domain model just to make test data easier to create.
Use locales when the scenario needs them
Bogus includes locale-specific data sets. Pass a locale to the faker when names, addresses, or other generated values need to resemble a particular region:
var germanCustomerFaker = new Faker<Customer>("de")
.RuleFor(customer => customer.FirstName, f => f.Name.FirstName())
.RuleFor(customer => customer.LastName, f => f.Name.LastName())
.RuleFor(customer => customer.CountryCode, _ => "DE");
Locales improve the shape of sample data, but they do not make it valid for every business or regulatory scenario. If a postal code, tax number, phone number, or address must satisfy a specific country’s rules, add an explicit domain generator and validate it independently.
When Bogus is a good fit
Bogus is a strong fit when you need:
- Unit-test inputs with varied values.
- Integration-test datasets for APIs, databases, or message handlers.
- Seed data for local development and demos.
- Pagination, filtering, sorting, and search scenarios with enough rows to expose mistakes.
- Load-test input prepared before the run, where values need to look like the domain model.
- Reproducible examples that should not contain real customer data.
The biggest benefit is lower fixture maintenance. Instead of copying a large JSON document into every test, you can describe the few invariants that matter and generate the rest.
It also makes edge cases easier to express. A fixture can generate a normal customer by default, then override one rule for a specific scenario:
var suspendedCustomer = CustomerFixtures.Create()
.RuleFor(customer => customer.Status, _ => CustomerStatus.Suspended)
.RuleFor(customer => customer.Email, _ => "suspended-user@test.invalid")
.Generate();
This reuses the default rules from the shared fixture pattern shown below, then overrides only the values relevant to the scenario. Use fixed values for the behavior under test and randomness for the fields that are incidental.
When Bogus is not the right tool
Do not use Bogus as a substitute for the following:
- A mock library. Bogus creates objects and values. It does not configure a mocked service or verify an interaction.
- A contract fixture. If a test documents an exact API payload, an explicit hand-written object is often clearer than generated data.
- Production customer data. Fake values can look realistic, but they are not anonymized copies of real data and may still create operational risk if sent to external systems.
- A domain validator. Bogus may generate a plausible value without satisfying all of your business rules. Test the validator with intentional valid and invalid cases.
- A security or payment simulator. A generated card number, account number, or identity number is not permission to call a real provider. Keep integrations behind test doubles or provider sandboxes.
- A replacement for boundary cases. Random generation may never produce the exact empty, maximum, duplicate, expired, or malformed value your test needs. Add explicit examples for those cases.
Randomness also makes a poor foundation for a test that should be completely obvious to the next person reading it. If the important input is three strings and one status, write those values directly.
A practical fixture shape
For a larger test suite, keep faker definitions in a dedicated factory or fixture class instead of rebuilding them inside every test:
public static class CustomerFixtures
{
public static Faker<Customer> Create()
{
var nextId = 1;
return new Faker<Customer>("en")
.StrictMode(true)
.RuleFor(customer => customer.Id, _ => nextId++)
.RuleFor(customer => customer.FirstName, f => f.Name.FirstName())
.RuleFor(customer => customer.LastName, f => f.Name.LastName())
.RuleFor(customer => customer.Email, f => f.Internet.ExampleEmail())
.RuleFor(customer => customer.CountryCode, f => f.Address.CountryCode())
.RuleFor(customer => customer.Status, f => f.PickRandom<CustomerStatus>())
.RuleFor(customer => customer.CreatedAtUtc,
f => f.Date.PastOffset(2).UtcDateTime);
}
}
Tests can then start with a clear default and override only what matters:
var customer = CustomerFixtures.Create()
.RuleFor(c => c.Status, _ => CustomerStatus.Suspended)
.Generate();
This is a good balance between convenience and control. The shared fixture owns the defaults; the test still makes its scenario-specific assumptions visible.
Benefits and tradeoffs
The benefits are practical:
- Less repetitive setup code.
- More variation than a small set of hand-written fixtures.
- Better-looking local development data.
- Explicit relationships between generated objects.
- Optional repeatability when debugging.
- Reusable rules across unit tests, integration tests, and seeders.
The tradeoffs matter too:
- A fixture can become difficult to understand if it contains too many hidden rules.
- Random output can make failures harder to reproduce if you do not capture a seed.
- Generated data may be plausible without being valid for your business domain.
- Large generated graphs can make tests slow and obscure the behavior under test.
- Global seeding can introduce coupling between tests.
The solution is to keep fakers small, use explicit overrides for important behavior, capture seeds when diagnosing failures, and assert invariants instead of snapshots of random output.
Conclusion
Bogus is most useful when the problem is not “how do I mock this dependency?” but “how do I create enough controlled input to exercise this code?”
Start with a Faker<T>, add rules for the fields that matter, enable StrictMode for shared fixtures, compose related objects from existing generated values, and use a seed when you need to reproduce a failure.
Use it for test data, development data, demos, and load-test inputs. Do not use it as evidence that your business rules, external integrations, or security boundaries work. Those still need explicit tests.