How can I share state between Bogus (C# Faker) rules?

3.9k views Asked by At

I'm using Bogus to generate test data but I have some fields that depend on parts of another object (that I want to be chosen randomly for each generation) but that have to be consistent with each other.

That's probably not the best explanation so hopefully this example explains it better.

I have an Order which includes the ID and currency from a Customer.

public class Customer
{
    public Guid Id { get; set; }
    public string Currency { get; set; }
}

public class Order
{
    public Guid Id { get; set; }
    public Guid CustomerId { get; set; } // The ID of a customer
    public string Currency { get; set; } // The currency for the customer identified by CustomerId
    public decimal Amount { get; set; }
}

I can generate some customers using:

var customerFaker = 
    new Faker<Customer>()
    .StrictMode(true)
    .RuleFor("Id", f => f.Random.Guid())
    .RuleFor("Id", f => f.Finance.Currency());

var customers = customerFaker.Generate(10);

But I get stuck when it comes to "sharing" the customer that's been chosen between rules in the order generator:

var orderFaker =
    new Faker<Order>()
    .StrictMode(true)
    .RuleFor("Id", f => f.Random.Guid())
    .RuleFor("Amount", f => f.Finance.Amount())
    .RuleFor("CustomerId", f => f.PickRandom(customers).Id)
    //How do I share the Customer that's been chosen to use it in the rule below?
    .RuleFor("Currency", f => f.PickRandom(customers).Currency);

I've come up with a few less-than-ideal ways of doing it (like instantiating a new Faker each time and passing in a random customer) but I'm working with quite complicated objects and dependencies so I'd like to avoid that if possible.

My current thinking is that the best way might be to extend the Order class to be able to store the Customer and then cast it back to being an order later. I'd like to avoid this if possible given the number of models I'll need to do this for.

public class OrderWithCustomer : Order
{
    public Customer Customer { get; set; }
}

var orderWithCustomerFaker =
    new Faker<OrderWithCustomer>()
    .StrictMode(true)
    .RuleFor("Id", f => f.Random.Guid())
    .RuleFor("Amount", f => f.Finance.Amount())
    .RuleFor("Customer", f => f.PickRandom(customers))
    .RuleFor("CustomerId", (f, o) => o.Customer.Id)
    .RuleFor("Currency", (f, o) => o.Customer.Currency);

var orders = 
    orderWithCustomerFaker
    .Generate(10)
    .Select(withCustomer => (Order)withCustomer);
2

There are 2 answers

2
Tatranskymedved On

While Bogus provide great sake of generating all the random data, when you need to link based on existing relation, you don't want to get random stuff again. Instead, in this late step you want to select existing data based on previous assignment.

At first you are generating all your customers as List<Customer>:

var customerFaker = new Faker<Customer>()
    .StrictMode(true)
    .RuleFor("Id", f => f.Random.Guid())
    .RuleFor("Id", f => f.Finance.Currency());

var customers = customerFaker.Generate(10);

You can just access that list directly and match/find inside the required value (via custom method or with Linq):

using System.Linq; //for linq method, add at the top of file

var orderFaker = new Faker<Order>()
    .StrictMode(true)
    .RuleFor("Id", f => f.Random.Guid())
    .RuleFor("Amount", f => f.Finance.Amount())
    .RuleFor("CustomerId", f => f.PickRandom(customers).Id)

    //Assuming that you implement your GetById() method or something similar
    .RuleFor("Currency", (f,o) => customers.GetById(o.CustomerId).Currency);

    //Or directly by using linq
    .RuleFor("Currency", (f,o) => customers.First(c => c.Id == o.CustomerId).Currency);

This all should work, because 2nd argument in RuleFor() is a setter which assigns the value to property. You could even do this:

var orderFaker = new Faker<Order>()
    .RuleFor("Currency", f => "EUR");
0
Vasoli On

You can make class extended from Faker and add a field to that class witch can hold customer. Like this:

public class Customer
{
    public Guid Id { get; set; }
    public string Currency { get; set; }
    public List<Order> Orders { get; set; }
}

public class Order
{
    public Guid Id { get; set; }
    public Guid CustomerId { get; set; } // The ID of a customer
    public string Currency { get; set; } // The currency for the customer identified by CustomerId
    public decimal Amount { get; set; }
}



public sealed class OrderFaker : Faker<Order>
{
    private Customer? _customer;
    public OrderFaker()
    {
        _customer = null;
            
        StrictMode(true)
            .RuleFor(o => o.Id, f => f.Random.Guid())
            .RuleFor(o => o.CustomerId, _ => _customer!.Id)
            .RuleFor(o => o.Amount, f => f.Finance.Amount())
            .RuleFor(o => o.Currency, f =>  _customer!.Currency);
    }
    
    public OrderFaker With(Customer customer)
    {
        _customer = customer;
        return this;
    }
}

public sealed class CustomerFaker : Faker<Customer>
{
    private readonly OrderFaker _orderFaker = new();
    public CustomerFaker()
    {

        StrictMode(true)
            .RuleFor(c => c.Id, f => f.Random.Guid())
            .RuleFor(c => c.Currency, f => f.Finance.Currency().Code)
            .RuleFor(c => c.Orders, (f, c) => _orderFaker.With(c).Generate(f.Random.Int(3, 5)).ToList());
    }
}

After that you ca call new CustomerFaker().Generate(150) to generate 150 customers with 3 to 5 orders linked with each other.

Edit: Fixing currency.