3

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);
John
  • 76
  • 1
  • 6
  • There is also other way around, it would be similar to what you've described, but in `Order` class you would have reference only to `Customer`. `CustomerId` and `Currency` would be omitted. Later, when you would need currency or CustID, you would just access them via the reference like `someOrder.Customer.Id` or `someOrder.Customer.Currency`. – Tatranskymedved Jan 25 '21 at 20:40
  • Unfortunately that isn't possible as I'm working within a fixed set of models that I need to conform to. – John Jan 25 '21 at 22:26

2 Answers2

3

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");
Tatranskymedved
  • 4,194
  • 3
  • 21
  • 47
  • I was hoping to avoid doing this as I don't actually have a list of customers, I'm injecting an API data provider that I'd like to avoid having to call for each generation (mostly because it means I need to write a bunch of code to cache the requests). I guess I was hoping to be able to do something like `Faker.SetExtra("customer", Faker.PickRandom(customers))` and use it later with `Faker.GetExtra("customer")` inside one of the setter lambdas – John Jan 25 '21 at 22:19
  • @John In that case you can still fallback to that previous solution (I've mentioned it in comments under the question). That would make the work as well. You can add some properties (setters/getters), that would handle that. E.g. if you assign `Customer`, in behind it will assign CustomerId and Currency properties. – Tatranskymedved Jan 25 '21 at 22:23
0

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.

Vasoli
  • 77
  • 7