Creating Domain-Driven Test Data With Bogus

December 12, 2023#Software Development
Article
Author image.

Kyle McMaster, Senior Consultant

This article is part of a series on leveling up your testing skills

Bogus is a .NET library for generating test data in C#, F#, and VB.NET. It’s a great tool for creating test data for your unit and integration tests. Bogus is packed with data sets for common domains like Commerce, Finance, and Company information. It is actually a port of the faker.js library to C#. It also includes helpers for generating random yet meaningful data from .NET types like DateTime as well as your own complex types. It even includes built-in support for customization when considering more advanced topics like localization. In this post, we’ll look at how to use Bogus to create test data for a domain-driven application. Let’s get started! 🎉

Getting Started

To get started using Bogus in any test project, install the Bogus NuGet package into your project.

dotnet add package Bogus

Right out of the box, Bogus can begin generating test data for us. Let’s take a look at a simple example of generating a custom type with a handful of properties. We use the Faker<T> constructor to instantiate a new instance of the Faker<T> class. We then call the Generate() method to create an instance of the BogusEntity class.

public class BogusEntity
{
  public Guid Id { get; set; }
  public DateTime DateofBirth { get; set; }
  public string? Name { get; set; }
}

var faker = new Faker<BogusEntity>();
var entity = faker.Generate();

This will give us some surprising results. 🤔

{
    "Id": "00000000-0000-0000-0000-000000000000",
    "Name": null,
    "BirthDate": "0001-01-01T00:00:00"
}

Without any configuration, Bogus generates the default value for any Type such as 0 for int. We must customize each property by using the RuleFor() method to specify how each property should be generated if we would like non-default behavior. For types like int, bool, Guid, etc, we can use the built-in generator Random to create a random value. To avoid missing any properties, we can optionally turn on strict mode by calling the StrictMode(true) method. This will throw an exception if a property is not configured with a rule.

var faker = new Faker<BogusEntity>()
  .RuleFor(o => o.Id, f => f.Random.Guid())
  .RuleFor(o => o.Name, f => f.Name.FullName())
  .RuleFor(o => o.DateOfBirth, f => f.Date.Past());

var entity = faker.Generate();

// Generates something like this:
//{
//  "Id": "f5ec1d0f-f1de-398b-1c2b-592a4921cc04",
//  "DateOfBirth": "2023-10-31T22:04:22.7426011-04:00",
//  "Name": "Lora Kuvalis"
//}

var strictFaker = new Faker<BogusEntity>()
  .StrictMode(true)
  .RuleFor(o => o.Id, f => f.Random.Guid())
  .RuleFor(o => o.Name, f => f.Random.String());

// Throws a Bogus.ValidationException since DateOfBirth is not configured
var strictEntity = strictFaker.Generate(); 

Notice in the example above that for the first Faker<BogusEntity> we use the properties Name and Date in the mapping expression. These properties are actually data sets that Bogus provides out of the box. We can use these data sets to generate random yet meaningful data for our test objects. These data sets paired with the ability to customize and create our own domain data make Bogus a powerful tool for generating test data. This is especially important in domain-driven applications where the data is often complex and requires a lot of thought to create meaningful test data. A complete list of the built-in data sets can be found in the Bogus GitHub repository.

Creating Test Data For Domain-Driven Applications

The following functional test comes from the Ardalis.CleanArchitecture repository. It verifies the ContributorList API endpoint returns a list of Contributors with a count of two.

[Fact]
public async Task ReturnsTwoContributors()
{
  var result = await _client.GetAndDeserializeAsync<ContributorListResponse>("/Contributors");

  Assert.Equal(2, result.Contributors.Count);
  Assert.Contains(result.Contributors, i => i.Name == SeedData.Contributor1.Name);
  Assert.Contains(result.Contributors, i => i.Name == SeedData.Contributor2.Name);
}

Behind the scenes, this test is using the SeedData class to create the test data. The SeedData class is responsible for creating the Contributor entities and saving them to a SQLite database for functional testing.

public static class SeedData
{
  public static readonly Contributor Contributor1 = new(
      email: "Email1@Microsoft.com",
      firstName: "FirstName1",
      lastName: "LastName1",
      followers: 1,
      following: 2,
      stars: 3,
      status: ContributorStatus.NotSet.Name);
  public static readonly Contributor Contributor2 = new(
      email: "Email2@Microsoft.com",
      firstName: "FirstName2",
      lastName: "LastName2",
      followers: 4,
      following: 5,
      stars: 6,
      status: ContributorStatus.NotSet.Name);

  public static void PopulateTestData(AppDbContext dbContext)
  {
    foreach (var item in dbContext.Contributors)
    {
      dbContext.Remove(item);
    }
    dbContext.SaveChanges();

    dbContext.Contributors.Add(Contributor1);
    dbContext.Contributors.Add(Contributor2);

    dbContext.SaveChanges();
  }

  // A few other methods omitted for brevity
}

Manually constructing the two seeded Contributors works well for proof of concept applications or Entities with small amounts of properties but can become cumbersome to maintain as the number of properties grows or additional relationships are added to the entity. The test data will likely decline in value as more and more seeded values are required (How many times have you seen “Test User 1”, “Test User 2”, etc in your tests?). Bogus can help us here by not only providing us with realistic values but also simplifying the creation of the Contributor entities. We can create a ContributorFaker that will encapsulate all our Contributor-related customizations. Notice that since Contributor is an entity with private setters and no parameterless constructor, we must use the CustomInstantiator() method to create the entity. This method still provides access to the Faker generator so we can still use Random, Person, and other built-in generators. We can even use the PickRandom<T>() method to select a random value from a list of values.

This is where we can use our own data sets to create realistic test data for our domain-driven application. Since Status has a defined set of values that are captured in an enum (or rather a SmartEnum), we can call PickRandom<ContributorStatus>(ContributorStatus.List) which returns a random ContributorStatus from a supplied IEnumerable<ContributorStatus> collection. In this case, we pass in all possible values using the built-in ContributorStatus.List property. Finally, we would like to be reasonably sure we’re not creating Contributors with duplicate Ids so we can use the RuleFor() method to create a unique Id for each Contributor by using a private field to track the Id and incrementing it each time a new Contributor is created. Note that each instance of a ContributorFaker will have its own private field so the Id will be unique for each instance, if we were to create multiple instances of ContributorFaker in the same test, we would need to use a static field to track the Id or come up with another way to ensure the Id is unique.

public class ContributorFaker : Faker<Contributor>
{
  private int _initialId = 1;

  public ContributorFaker()
  {
    CustomInstantiator(f => new Contributor(
      email: f.Person.Email,
      firstName: f.Person.FirstName,
      lastName: f.Person.LastName,
      followers: f.Random.Int(0, 100),
      following: f.Random.Int(0, 100),
      stars: f.Random.Int(0, 100),
      status: f.PickRandom<ContributorStatus>(ContributorStatus.List).Name))
      .RuleFor(o => o.Id, f => _initialId++);
  }
}

With the ContributorFaker implemented, the SeedData class can now reduce its setup to the following lines of code. This is a significant reduction in the arrangement of code and will be much easier to maintain as the number of properties on the Contributor entity grows. We can also reuse the ContributorFaker in other tests that require a Contributor entity similar to a Builder or Factory.

public static class SeedData
{
  private static readonly ContributorFaker _contributorFaker = new();
  public static Contributor Contributor1 = _contributorFaker.Generate();
  public static Contributor Contributor2 = _contributorFaker.Generate();

  // Remaining code stays the same
}

One other feature of the Faker<T> class that can be useful is generating a collection of entities. This can be done by calling the Generate() method with a count. This is useful when we need to create a collection of entities for a one-to-many relationship or we’re seeding a database with a large amount of data. For instance, to create a collection of 100 Contributors, we can call Generate(100) which will give us 100 unique Contributor instances. I won’t show all 100 for the sake of brevity, but here are the first 4 as an example of what a few of those might look like. 👀

{
  "Id": 1,
  "Email": "Tom_Gibson97@yahoo.com",
  "FirstName": "Tom",
  "LastName": "Gibson",
  "Followers": 3,
  "Following": 97,
  "Stars": 86,
  "Status": "NotSet",
},
{
  "Id": 2,
  "Email": "Danny29@yahoo.com",
  "FirstName": "Danny",
  "LastName": "Reichert",
  "Followers": 43,
  "Following": 43,
  "Stars": 30,
  "Status": "CoreTeam",
},
{
  "Id": 3,
  "Email": "Cornelius.Koss@hotmail.com",
  "FirstName": "Cornelius",
  "LastName": "Koss",
  "Followers": 94,
  "Following": 89,
  "Stars": 11,
  "Status": "CoreTeam",
},
{
  "Id": 4,
  "Email": "Darla.Kub@gmail.com",
  "FirstName": "Darla",
  "LastName": "Kub",
  "Followers": 34,
  "Following": 86,
  "Stars": 26,
  "Status": "Community",
}

Conclusion

Bogus is an extremely powerful yet configurable tool that can be customized to provide exactly the data needed for all kinds of test scenarios. If you’re interested in learning more, I highly recommend checking out the available data sets, localization features, and The great C# Example which demonstrates many of the features of Bogus in one snippet. All of the code from this article is available to inspect and run in this repository of Bogus Samples.

References


Copyright © 2024 NimblePros - All Rights Reserved