15 minlesson

Generating Test Data with Bogus

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:

bash
1dotnet 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:

csharp
1using Bogus;
2
3var 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");
10
11var 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:

csharp
1Randomizer.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:

csharp
1var 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:

csharp
1var 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:

csharp
1var 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 switch
6 {
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:

csharp
1var 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};
10
11var 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:

csharp
1var 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:

csharp
1var 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:

csharp
1using Bogus;
2using ParcelTracking.Infrastructure.Data;
3using ParcelTracking.Domain.Entities;
4using ParcelTracking.Domain.Enums;
5
6namespace ParcelTracking.Infrastructure.Data;
7
8public static class DataSeeder
9{
10 public static async Task SeedAsync(ParcelTrackingDbContext db)
11 {
12 if (await db.Parcels.AnyAsync())
13 return; // Already seeded
14
15 Randomizer.Seed = new Random(42);
16
17 // 1. Generate 50 addresses
18 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());
33
34 var addresses = addressFaker.Generate(50);
35 db.Addresses.AddRange(addresses);
36
37 // 2. Generate 200 parcels
38 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);
58
59 var parcels = parcelFaker.Generate(200);
60 db.Parcels.AddRange(parcels);
61
62 // 3. Generate 2-5 tracking events per parcel
63 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 switch
67 {
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");
77
78 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 }
91
92 // 4. Generate 1-3 content items per parcel
93 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 };
102
103 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"));
115
116 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 }
126
127 // 5. Generate delivery confirmations for delivered parcels
128 var deliveredParcels = parcels
129 .Where(p => p.Status == ParcelStatus.Delivered);
130
131 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));
140
141 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 }
148
149 // 6. Generate 30 parcel watchers with random parcel assignments
150 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));
156
157 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);
165
166 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:

csharp
1var app = builder.Build();
2
3if (args.Contains("--seed"))
4{
5 using var scope = app.Services.CreateScope();
6 var db = scope.ServiceProvider
7 .GetRequiredService<ParcelTrackingDbContext>();
8 await DataSeeder.SeedAsync(db);
9 Console.WriteLine("Database seeded successfully.");
10 return;
11}

Run it with:

bash
1dotnet 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 with RuleFor to 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 DataSeeder class 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.