Generating Test Data with Bogus
Testing an API with one or two hand-crafted records is tedious and unrealistic. In this lesson, you learn how to generate hundreds of realistic test records using the Bogus library so your database looks like a real carrier system from the start.
Why Generate Test Data
Manually inserting test data through SQL scripts or seed methods is slow and produces obviously fake records like "Test Parcel 1" shipped to "123 Fake Street." This causes several problems:
- Pagination, filtering, and sorting are hard to test with only a handful of records
- Edge cases like parcels with many tracking events or content items never appear
- Demo environments look unconvincing to stakeholders
- Performance testing requires volume that manual entry cannot produce
Bogus solves all of these by generating realistic, randomized data that follows the rules you define.
Installing Bogus
Add the Bogus NuGet package to the project:
bash1dotnet add src/ParcelTracking.Infrastructure package Bogus
Bogus is a .NET port of the popular faker.js library. It generates realistic data for names, addresses, phone numbers, dates, and much more.
How Bogus Works
The core concept is a Faker<T> that defines rules for generating instances of a class:
csharp1using Bogus;23var faker = new Faker<Address>()4 .RuleFor(a => a.Id, f => Guid.NewGuid())5 .RuleFor(a => a.Street1, f => f.Address.StreetAddress())6 .RuleFor(a => a.City, f => f.Address.City())7 .RuleFor(a => a.State, f => f.Address.StateAbbr())8 .RuleFor(a => a.PostalCode, f => f.Address.ZipCode())9 .RuleFor(a => a.CountryCode, f => "US");1011var addresses = faker.Generate(50);
Each call to Generate() creates a new instance with randomized values. The f parameter gives you access to Bogus's data generators organized by category: f.Address, f.Phone, f.Internet, f.Commerce, f.Date, and many more.
You can set a seed for reproducible results:
csharp1Randomizer.Seed = new Random(12345);
With a fixed seed, every run produces the same data. This is useful for consistent test environments and debugging.
Address Faker
The Address entity maps naturally to Bogus's built-in address generators:
csharp1var addressFaker = new Faker<Address>()2 .RuleFor(a => a.Id, f => Guid.NewGuid())3 .RuleFor(a => a.Street1, f => f.Address.StreetAddress())4 .RuleFor(a => a.Street2, f => f.Random.Bool(0.3f) ? f.Address.SecondaryAddress() : null)5 .RuleFor(a => a.City, f => f.Address.City())6 .RuleFor(a => a.State, f => f.Address.StateAbbr())7 .RuleFor(a => a.PostalCode, f => f.Address.ZipCode())8 .RuleFor(a => a.CountryCode, f => "US")9 .RuleFor(a => a.IsResidential, f => f.Random.Bool(0.7f))10 .RuleFor(a => a.ContactName, f => f.Name.FullName())11 .RuleFor(a => a.CompanyName, (f, a) => a.IsResidential ? null : f.Company.CompanyName())12 .RuleFor(a => a.Phone, f => f.Phone.PhoneNumber("###-###-####"))13 .RuleFor(a => a.Email, f => f.Internet.Email());
Notice the conditional logic: 30% of addresses get a Street2, and CompanyName is only set for commercial addresses. Bogus lets you reference previously generated properties using the (f, a) overload, where a is the partially built object.
Parcel Faker
The Parcel faker references pre-generated addresses and builds a tracking number in a realistic format:
csharp1var parcelFaker = new Faker<Parcel>()2 .RuleFor(p => p.Id, f => Guid.NewGuid())3 .RuleFor(p => p.TrackingNumber, f =>4 $"PKG-{f.Date.Recent(30):yyyyMMdd}-{f.Random.AlphaNumeric(6).ToUpper()}")5 .RuleFor(p => p.Description, f => f.Commerce.ProductName())6 .RuleFor(p => p.ServiceType, f => f.PickRandom<ServiceType>())7 .RuleFor(p => p.Status, f => f.PickRandom<ParcelStatus>())8 .RuleFor(p => p.ShipperAddressId, f => f.PickRandom(addresses).Id)9 .RuleFor(p => p.RecipientAddressId, f => f.PickRandom(addresses).Id)10 .RuleFor(p => p.Weight, f => f.Random.Decimal(0.5m, 50m))11 .RuleFor(p => p.WeightUnit, f => WeightUnit.Lb)12 .RuleFor(p => p.Length, f => f.Random.Decimal(5m, 40m))13 .RuleFor(p => p.Width, f => f.Random.Decimal(5m, 30m))14 .RuleFor(p => p.Height, f => f.Random.Decimal(2m, 20m))15 .RuleFor(p => p.DimensionUnit, f => DimensionUnit.In)16 .RuleFor(p => p.DeclaredValue, f => f.Finance.Amount(10m, 2000m))17 .RuleFor(p => p.Currency, f => "USD")18 .RuleFor(p => p.EstimatedDeliveryDate, f => f.Date.SoonOffset(7))19 .RuleFor(p => p.CreatedAt, f => f.Date.RecentOffset(30))20 .RuleFor(p => p.UpdatedAt, (f, p) => p.CreatedAt);
The tracking number format PKG-20250215-A1B2C3 combines a date and random alphanumeric suffix, matching the format used throughout the course. f.PickRandom(addresses).Id selects a random address from the pre-generated list, creating realistic foreign key references.
TrackingEvent Faker
Each parcel needs between 2 and 5 tracking events that tell a believable story:
csharp1var eventFaker = new Faker<TrackingEvent>()2 .RuleFor(te => te.Id, f => Guid.NewGuid())3 .RuleFor(te => te.Timestamp, f => f.Date.RecentOffset(14))4 .RuleFor(te => te.EventType, f => f.PickRandom<EventType>())5 .RuleFor(te => te.Description, (f, te) => te.EventType switch6 {7 EventType.PickedUp => "Package picked up from sender",8 EventType.ArrivedAtFacility => $"Arrived at {f.Address.City()} facility",9 EventType.DepartedFacility => $"Departed {f.Address.City()} facility",10 EventType.OutForDelivery => "Out for delivery",11 EventType.Delivered => "Delivered to recipient",12 _ => f.Lorem.Sentence()13 })14 .RuleFor(te => te.LocationCity, f => f.Address.City())15 .RuleFor(te => te.LocationState, f => f.Address.StateAbbr())16 .RuleFor(te => te.LocationCountry, f => "US");
The switch expression generates descriptions that match the event type, making the data look realistic when displayed in tracking history views.
ParcelContentItem Faker
Content items use realistic HS codes and product descriptions:
csharp1var hsCodePairs = new[]2{3 ("8471.30", "Laptop computer"),4 ("8517.13", "Smartphone"),5 ("6110.20", "Cotton sweater"),6 ("9503.00", "Children's toy"),7 ("3304.99", "Skincare product"),8 ("8528.72", "LED monitor")9};1011var contentFaker = new Faker<ParcelContentItem>()12 .RuleFor(ci => ci.Id, f => Guid.NewGuid())13 .RuleFor(ci => ci.HsCode, f => f.PickRandom(hsCodePairs).Item1)14 .RuleFor(ci => ci.Description, (f, ci) =>15 hsCodePairs.First(p => p.Item1 == ci.HsCode).Item2)16 .RuleFor(ci => ci.Quantity, f => f.Random.Int(1, 5))17 .RuleFor(ci => ci.UnitValue, f => f.Finance.Amount(10m, 500m))18 .RuleFor(ci => ci.Currency, f => "USD")19 .RuleFor(ci => ci.Weight, f => f.Random.Decimal(0.1m, 5m))20 .RuleFor(ci => ci.WeightUnit, f => WeightUnit.Lb)21 .RuleFor(ci => ci.CountryOfOrigin, f => f.PickRandom("CN", "US", "DE", "JP", "KR"));
Using a lookup array of HS code and description pairs ensures the descriptions match their codes, just like real customs declarations.
DeliveryConfirmation Faker
Delivery confirmations are only created for parcels with Delivered status:
csharp1var confirmationFaker = new Faker<DeliveryConfirmation>()2 .RuleFor(dc => dc.Id, f => Guid.NewGuid())3 .RuleFor(dc => dc.ReceivedBy, f => f.Name.FullName())4 .RuleFor(dc => dc.DeliveryLocation, f =>5 f.PickRandom("Front door", "Reception desk", "Mailroom", "Side gate", "Garage"))6 .RuleFor(dc => dc.SignatureImage, f => Convert.ToBase64String(f.Random.Bytes(64)))7 .RuleFor(dc => dc.DeliveredAt, f => f.Date.RecentOffset(7));
The SignatureImage uses random bytes encoded as Base64 to simulate a signature. In a real application, this would be actual signature capture data.
ParcelWatcher Faker
Parcel watchers represent people who want notifications about specific parcels. Each watcher tracks a random subset of parcels through the many-to-many relationship:
csharp1var watcherFaker = new Faker<ParcelWatcher>()2 .RuleFor(w => w.Id, f => Guid.NewGuid())3 .RuleFor(w => w.Email, f => f.Internet.Email())4 .RuleFor(w => w.Name, f => f.Random.Bool(0.8f) ? f.Name.FullName() : null)5 .RuleFor(w => w.CreatedAt, f => f.Date.RecentOffset(30));
After generating watchers, you assign random parcels to each watcher's Parcels collection. This populates the EF Core auto-generated join table.
The Complete Seed Method
Combine all the fakers into a single DataSeeder class:
csharp1using Bogus;2using ParcelTracking.Infrastructure.Data;3using ParcelTracking.Domain.Entities;4using ParcelTracking.Domain.Enums;56namespace ParcelTracking.Infrastructure.Data;78public static class DataSeeder9{10 public static async Task SeedAsync(ParcelTrackingDbContext db)11 {12 if (await db.Parcels.AnyAsync())13 return; // Already seeded1415 Randomizer.Seed = new Random(42);1617 // 1. Generate 50 addresses18 var addressFaker = new Faker<Address>()19 .RuleFor(a => a.Id, f => Guid.NewGuid())20 .RuleFor(a => a.Street1, f => f.Address.StreetAddress())21 .RuleFor(a => a.Street2, f => f.Random.Bool(0.3f)22 ? f.Address.SecondaryAddress() : null)23 .RuleFor(a => a.City, f => f.Address.City())24 .RuleFor(a => a.State, f => f.Address.StateAbbr())25 .RuleFor(a => a.PostalCode, f => f.Address.ZipCode())26 .RuleFor(a => a.CountryCode, f => "US")27 .RuleFor(a => a.IsResidential, f => f.Random.Bool(0.7f))28 .RuleFor(a => a.ContactName, f => f.Name.FullName())29 .RuleFor(a => a.CompanyName, (f, a) =>30 a.IsResidential ? null : f.Company.CompanyName())31 .RuleFor(a => a.Phone, f => f.Phone.PhoneNumber("###-###-####"))32 .RuleFor(a => a.Email, f => f.Internet.Email());3334 var addresses = addressFaker.Generate(50);35 db.Addresses.AddRange(addresses);3637 // 2. Generate 200 parcels38 var parcelFaker = new Faker<Parcel>()39 .RuleFor(p => p.Id, f => Guid.NewGuid())40 .RuleFor(p => p.TrackingNumber, f =>41 $"PKG-{f.Date.Recent(30):yyyyMMdd}-{f.Random.AlphaNumeric(6).ToUpper()}")42 .RuleFor(p => p.Description, f => f.Commerce.ProductName())43 .RuleFor(p => p.ServiceType, f => f.PickRandom<ServiceType>())44 .RuleFor(p => p.Status, f => f.PickRandom<ParcelStatus>())45 .RuleFor(p => p.ShipperAddressId, f => f.PickRandom(addresses).Id)46 .RuleFor(p => p.RecipientAddressId, f => f.PickRandom(addresses).Id)47 .RuleFor(p => p.Weight, f => f.Random.Decimal(0.5m, 50m))48 .RuleFor(p => p.WeightUnit, f => WeightUnit.Lb)49 .RuleFor(p => p.Length, f => f.Random.Decimal(5m, 40m))50 .RuleFor(p => p.Width, f => f.Random.Decimal(5m, 30m))51 .RuleFor(p => p.Height, f => f.Random.Decimal(2m, 20m))52 .RuleFor(p => p.DimensionUnit, f => DimensionUnit.In)53 .RuleFor(p => p.DeclaredValue, f => f.Finance.Amount(10m, 2000m))54 .RuleFor(p => p.Currency, f => "USD")55 .RuleFor(p => p.EstimatedDeliveryDate, f => f.Date.SoonOffset(7))56 .RuleFor(p => p.CreatedAt, f => f.Date.RecentOffset(30))57 .RuleFor(p => p.UpdatedAt, (f, p) => p.CreatedAt);5859 var parcels = parcelFaker.Generate(200);60 db.Parcels.AddRange(parcels);6162 // 3. Generate 2-5 tracking events per parcel63 var eventFaker = new Faker<TrackingEvent>()64 .RuleFor(te => te.Id, f => Guid.NewGuid())65 .RuleFor(te => te.EventType, f => f.PickRandom<EventType>())66 .RuleFor(te => te.Description, (f, te) => te.EventType switch67 {68 EventType.PickedUp => "Package picked up from sender",69 EventType.ArrivedAtFacility => $"Arrived at {f.Address.City()} facility",70 EventType.OutForDelivery => "Out for delivery",71 EventType.Delivered => "Delivered to recipient",72 _ => f.Lorem.Sentence()73 })74 .RuleFor(te => te.LocationCity, f => f.Address.City())75 .RuleFor(te => te.LocationState, f => f.Address.StateAbbr())76 .RuleFor(te => te.LocationCountry, f => "US");7778 var random = new Randomizer();79 foreach (var parcel in parcels)80 {81 var count = random.Int(2, 5);82 var baseDate = parcel.CreatedAt;83 for (var i = 0; i < count; i++)84 {85 var evt = eventFaker.Generate();86 evt.ParcelId = parcel.Id;87 evt.Timestamp = baseDate.AddHours(i * 12);88 db.TrackingEvents.Add(evt);89 }90 }9192 // 4. Generate 1-3 content items per parcel93 var hsCodePairs = new[]94 {95 ("8471.30", "Laptop computer"),96 ("8517.13", "Smartphone"),97 ("6110.20", "Cotton sweater"),98 ("9503.00", "Children's toy"),99 ("3304.99", "Skincare product"),100 ("8528.72", "LED monitor")101 };102103 var contentFaker = new Faker<ParcelContentItem>()104 .RuleFor(ci => ci.Id, f => Guid.NewGuid())105 .RuleFor(ci => ci.HsCode, f => f.PickRandom(hsCodePairs).Item1)106 .RuleFor(ci => ci.Description, (f, ci) =>107 hsCodePairs.First(p => p.Item1 == ci.HsCode).Item2)108 .RuleFor(ci => ci.Quantity, f => f.Random.Int(1, 5))109 .RuleFor(ci => ci.UnitValue, f => f.Finance.Amount(10m, 500m))110 .RuleFor(ci => ci.Currency, f => "USD")111 .RuleFor(ci => ci.Weight, f => f.Random.Decimal(0.1m, 5m))112 .RuleFor(ci => ci.WeightUnit, f => WeightUnit.Lb)113 .RuleFor(ci => ci.CountryOfOrigin, f =>114 f.PickRandom("CN", "US", "DE", "JP", "KR"));115116 foreach (var parcel in parcels)117 {118 var count = random.Int(1, 3);119 for (var i = 0; i < count; i++)120 {121 var item = contentFaker.Generate();122 item.ParcelId = parcel.Id;123 db.ParcelContentItems.Add(item);124 }125 }126127 // 5. Generate delivery confirmations for delivered parcels128 var deliveredParcels = parcels129 .Where(p => p.Status == ParcelStatus.Delivered);130131 var confirmationFaker = new Faker<DeliveryConfirmation>()132 .RuleFor(dc => dc.Id, f => Guid.NewGuid())133 .RuleFor(dc => dc.ReceivedBy, f => f.Name.FullName())134 .RuleFor(dc => dc.DeliveryLocation, f => f.PickRandom(135 "Front door", "Reception desk", "Mailroom",136 "Side gate", "Garage"))137 .RuleFor(dc => dc.SignatureImage, f =>138 Convert.ToBase64String(f.Random.Bytes(64)))139 .RuleFor(dc => dc.DeliveredAt, f => f.Date.RecentOffset(7));140141 foreach (var parcel in deliveredParcels)142 {143 var confirmation = confirmationFaker.Generate();144 confirmation.ParcelId = parcel.Id;145 db.DeliveryConfirmations.Add(confirmation);146 parcel.ActualDeliveryDate = confirmation.DeliveredAt;147 }148149 // 6. Generate 30 parcel watchers with random parcel assignments150 var watcherFaker = new Faker<ParcelWatcher>()151 .RuleFor(w => w.Id, f => Guid.NewGuid())152 .RuleFor(w => w.Email, f => f.Internet.Email())153 .RuleFor(w => w.Name, f => f.Random.Bool(0.8f)154 ? f.Name.FullName() : null)155 .RuleFor(w => w.CreatedAt, f => f.Date.RecentOffset(30));156157 var watchers = watcherFaker.Generate(30);158 foreach (var watcher in watchers)159 {160 var watchCount = random.Int(1, 8);161 var watchedParcels = random.ListItems(parcels, watchCount);162 watcher.Parcels = watchedParcels;163 }164 db.Set<ParcelWatcher>().AddRange(watchers);165166 await db.SaveChangesAsync();167 }168}
The seeder is idempotent: it checks whether data already exists before inserting. The fixed seed (new Random(42)) ensures reproducible results across runs.
Running the Seeder
Call the seeder from Program.cs using a command-line argument pattern:
csharp1var app = builder.Build();23if (args.Contains("--seed"))4{5 using var scope = app.Services.CreateScope();6 var db = scope.ServiceProvider7 .GetRequiredService<ParcelTrackingDbContext>();8 await DataSeeder.SeedAsync(db);9 Console.WriteLine("Database seeded successfully.");10 return;11}
Run it with:
bash1dotnet run -- --seed
The --seed flag triggers seeding and exits immediately. This pattern keeps seeding separate from normal application startup, so you only run it when you need fresh test data.
After seeding, start the application normally with dotnet run and use the Scalar explorer to query parcels, tracking events, and content items. You should see 200 parcels with realistic tracking numbers, addresses, and customs data.
Summary
In this lesson, you learned how to:
- Install and configure the Bogus library for test data generation
- Build
Faker<T>instances withRuleForto generate realistic property values - Use conditional logic and cross-references between fakers for related entities
- Generate realistic addresses, parcels, tracking events, content items, delivery confirmations, and parcel watchers
- Create an idempotent
DataSeederclass that populates the entire domain model - Run the seeder using a command-line argument pattern in
Program.cs
With 50 addresses, 200 parcels, and hundreds of related records in the database, you have a realistic dataset for testing every feature you build in the remaining topics.