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

Ruleset behavior not working with multi-layer faker classes #468

Closed altmank closed 1 year ago

altmank commented 1 year ago

I'm trying to create objects where objects higher in a hierarchy may have to have properties generated by other faker classes. I want to support having the ruleset passed to the top level object propagate that ruleset down to child faker instances used in property faking.

I can't seem to get this to work and its behavior seems like a bug to me. Can you please take a quick look and evaluate if I'm approaching this wrong or if there is a defect here in ruleset functionality?

Example Linqpad script:

void Main()
{
    PersonWrapperFaker wrapperFaker = new PersonWrapperFaker();
    // I want all generation to happen among both the parent and all child faker objects to use the specified ruleset of "young"
    wrapperFaker.Generate("young").Dump("With wrapper");

    var personFaker = new PersonFaker();
    personFaker.Generate("young").Dump("Directly on child faker object");

}

public class PersonWrapper
{
    public Person Child { get; set; }
}

public class Person
{
    public bool IsOld { get; set; }

}

public class PersonWrapperFaker : Faker<PersonWrapper>
{
    public PersonWrapperFaker()
    {
        RuleFor(t => t.Child, new PersonFaker().Generate());

        //Have also tried:
        //RuleFor(t => t.Child, new PersonFaker().Generate("young"));
    }
}

public class PersonFaker : Faker<Person>
{
    public PersonFaker()
    {
        //default
        RuleFor(x => x.IsOld, f => true);

        RuleSet("young", rule =>
        {
            rule.RuleFor(x => x.IsOld, f => false);
        });
    }

}
bchavez commented 1 year ago

Hello @altmank, thank you very much for the LinqPad example, it makes providing help much easier when we can communicate with concrete code and examples.

The first problem is,

RuleFor(t => t.Child, new PersonFaker().Generate());

You should always specify 2 lambdas for .RuleFor. The .RuleFor with one lambda is for using constant values (that never change). It's almost always a bug using the overload with one lambda as above.

Rather, you should write rules like the following below:

RuleFor(t => t.Child, f => new PersonFaker().Generate());

The second problem is a misconception on how to flow state between Faker<T1> and Faker<T2>. The important thing to know about Faker<T> is that they are completely independent and isolated from one another. Faker<T1> knows nothing about Faker<T2> unless you carry the state along between them and explicitly use the state your asking for.

The reason why your PersonWrapperFaker is always .Child = null is that the ruleset for "young" is defined in Person, not PersonWrapperFaker. In simpler terms,

void Main()
{
   var f = new FooFaker();
   f.Generate("myrules").Dump();
   f.Generate("non-existing-ruleset").Dump(); // does nothing because the rulset is not defined
}

public class Foo
{
   public string Message {get;set;}
}

public class FooFaker : Faker<Foo>
{
   public FooFaker()
   {
      RuleSet("myrules", rule =>
      {
         rule.RuleFor(f => f.Message, f => "This 'myrules' ruleset exists");
      });
   }
}

image

Finally, expanding this further,

void Main()
{
   var parentFaker = new ParentFaker();
   // I want all generation to happen among both the parent and all
   // child faker objects to use the specified ruleset of "young"
   parentFaker.WithChildRuleset("young").Generate().Dump("With parent: young");
   parentFaker.WithChildRuleset("").Generate().Dump("With parent: none|default");

   var personFaker = new PersonFaker();
   personFaker.Generate("young").Dump("Directly on child faker object");
}

public class Parent
{
   public Person Child { get; set; }
}

public class Person
{
   public bool IsOld { get; set; }
}

public class ParentFaker : Faker<Parent>
{
   public ParentFaker()
   {
      RuleFor(t => t.Child, f => new PersonFaker().Generate(this.childRuleset));
   }

   string childRuleset = ""; // default child rulset

   public ParentFaker WithChildRuleset(string ruleset){
      this.childRuleset = ruleset;
      return this;
   }
}

public class PersonFaker : Faker<Person>
{
   public PersonFaker()
   {
      //default "" ruleset
      RuleFor(x => x.IsOld, f => true);

      RuleSet("young", rule =>
      {
         rule.RuleFor(x => x.IsOld, f => false);
      });
   }
}

Yields the following:

image

Hope that helps.

Thanks,
Brian Chavez