bchavez / Bogus

:card_index: A simple fake data generator for C#, F#, and VB.NET. Based on and ported from the famed faker.js.
Other
8.62k stars 491 forks source link

Alllow Generate JsonNode/Proxy of Faker<T> #458

Open lucasteles opened 1 year ago

lucasteles commented 1 year ago

The typed Faker class is great to create complex builder scenarios, the power of defining the shape of the type in parts as necessary is one of the best things to solve problems like test scenarios.

Something like this:

public class User
{
    public string Name { get; init; }
    public int Age { get; init; }
    public string Email { get; init; }
}

var builder = new Faker<User>()
    .RuleFor(x => x.Email, f => f.Person.Email)
    .RuleFor(x => x.Age, f => f.Random.Int(18, 50))
    .RuleFor(x => x.Name, f => f.Person.FirstName);

Things get a bit complicated when the Type has a complex constructor or a mix with required properties:

var builder = new Faker<User>()
    .RuleFor(x => x.Age, f => f.Random.Int(18, 50))
    .RuleFor(x => x.Name, f => f.Person.FirstName)
    .CustomInstantiator(f =>
        new User(f.Person.Email)
        {
            Name = // need to be set //,
            Age = // need to be set //,
        });

In this case, we kinda lost the power of member composability. If it was a custom builder I would have to create fields to hold values for the CustomInstantiator etc.

So would be nice to have a way to get the shape of the data without constructing the actual Type

A generic way to solve this could be to have a new method like GetJsonObject, which for the case above could be something like:

var builder = new Faker<User>()
    .RuleFor(x => x.Email, f => f.Person.Email)
    .RuleFor(x => x.Age, f => f.Random.Int(18, 50))
    .RuleFor(x => x.Name, f => f.Person.FirstName);

var values = builde.GenerateJsonNode();
/*
new JsonObject
    {
        ["Name"] = "Casey",
        ["Age"] = 22,
        ["Email"] = "Casey_Ebert22@hotmail.com",
    };
*/

So it could be used on the complex constructor, preserving the member composability:

var builder = new Faker<User>()
    .RuleFor(x => x.Email, f => f.Person.Email)
    .RuleFor(x => x.Age, f => f.Random.Int(18, 50))
    .RuleFor(x => x.Name, f => f.Person.FirstName)
    .CustomInstantiator(f => {
       var values = f.GetJsonObject();
       return new User((values["Email"].GetValue<string>())
        {
            Name = values["Name"].GetValue<string>(),
            Age = values["Age"].GetValue<int>(),
        });
   }

Another solution would be use Castle DynamicProxy to maintain the type contract but mocking the property values:

var builder = new Faker<User>()
    .RuleFor(x => x.Email, f => f.Person.Email)
    .RuleFor(x => x.Age, f => f.Random.Int(18, 50))
    .RuleFor(x => x.Name, f => f.Person.FirstName)
    .CustomInstantiator(f => {
       var proxy= f.GetObjectProxy();
       return new User(proxy.Email)
        {
            Name = proxy.Name,
            Age = proxy.Age
        });
   }

If the feature request is approved, would you be willing to submit a PR?
Yes

bchavez commented 1 year ago

Hi @lucasteles; could you give me a few concrete examples of what you're trying to accomplish? Full examples that can compile would help a lot.

I'm having some trouble understanding your request. The following seems to work fine:

void Main()
{
   var builder = new Faker<User>()
       .RuleFor(x => x.Email, f => f.Person.Email)
       .RuleFor(x => x.Age, f => f.Random.Int(18, 50))
       .RuleFor(x => x.Name, f => f.Person.FirstName);

   builder.Generate().Dump();
}

public class User
{
   public string Name { get; init; }
   public int Age { get; init; }
   public string Email { get; init; }
}

image

If you're finding that the following is too complex or unsightly:

var builder = new Faker<User>()
   .RuleFor(x => x.Age, f => f.Random.Int(18, 50))
   .RuleFor(x => x.Name, f => f.Person.FirstName)
   .CustomInstantiator(f =>
       new User(f.Person.Email)
       {
           Name = ...,
           Age = ...,
       });

Consider refactoring and using a non-typed Faker:

void Main()
{        
   User GenerateUser()
   {
      var f = new Faker();
      var user = new User(f.Person.Email)
               {
                  Name = f.Person.FirstName,
                  Age = f.Random.Int(18,50)
               };

      return user;
   }

   GenerateUser().Dump();
}

public class User
{
   public User(string email) => Email = email;

   public string Name { get; init; }
   public int Age { get; init; }
   public string Email { get; init; }
}

As you've indicated, Faker<T> has some limits before object design complexity gets in our way; and in such cases, we should consider a non-typed Faker as a suitable and reasonable alternative.