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.51k stars 483 forks source link

Fakers used inside Rules of other Fakers share the same object reference #543

Closed MihaiDaniel closed 2 months ago

MihaiDaniel commented 2 months ago

Bogus NuGet Package

35.5.0

.NET Version

.NET 8

Visual Studio Version

17.8.3

What operating system are you using?

Windows

What locale are you using with Bogus?

en_US

Problem Description

I need to generate fake data that has inner objects but these inner generated objects seem to share the reference in any of the contained generated objects.

The problem arises when using a builder-like pattern to use bogus to build unique objects for testing purposes. I use inside a Faker rules another Faker to generate inner objects. Say I have a MyClass and a MyInnerClass that is a property inside MyClass. I would like to generate so that MyInnerClass has unique values for any MyClass generated, but what happens is that all MyClass share the same reference to the latest generated MyInnerClass

See code example. The assert should fail, but it passes.

LINQPad Example or Reproduction Steps

    public class MyClass
    {
        public int Id { get; set; }

        public MyInnerClass MyInnerClass { get; set; }
    }

    public class MyInnerClass
    {
        public int Id { get; set; }
    }

    public class MyClassFaker
    {
        private Bogus.Faker<MyClass> faker = new();

        public MyClassFaker()
        {
            faker
                .RuleFor(mc => mc.Id, f => f.Random.Int())
                .RuleFor(mc => mc.MyInnerClass, new MyInnerClassFaker().Get());
        }

        public MyClass Get() { return faker.Generate(); }
    }

    public class MyInnerClassFaker
    {
        private Bogus.Faker<MyInnerClass> faker = new();

        public MyInnerClassFaker()
        {
            faker.RuleFor(mc => mc.Id, f => f.Random.Int());
        }

        public MyInnerClass Get() { return faker.Generate(); }
    }

        [Fact]
        public void Test()
        {
            MyClassFaker myClassFaker = new();
            MyClass class1 = myClassFaker.Get();
            MyClass class2 = myClassFaker.Get();

                // Assert should fail, but instead it passes because class1 and class2 share the same reference to the latest generated MyInnerClass
            Assert.NotEqual(class1.MyInnerClass.Id, class2.MyInnerClass.Id);
        }

Expected Behavior

Fakers used inside other Rules of other fakers should not share references.

Actual Behavior

Fakers used inside Rules of other fakers shares the same reference to the newest generated object.

Known Workarounds

I haven't found a workaround yet, just to manually set the inner objects.

Could you help with a pull-request?

Yes

bchavez commented 2 months ago

The problem is the declaration .RuleFor for the following inner class as a constant:

.RuleFor(mc => mc.MyInnerClass, new MyInnerClassFaker().Get());

The correct way to invoke the lambda upon each generaiton is to use the lambda overload; not the value/constant overload:

.RuleFor(mc => mc.MyInnerClass, f => new MyInnerClassFaker().Get());
                               ^^^^^^

You need to make sure if you want something done on each generation instance, that you use lambdas .RuleFor( ... , f => f); not .RuleFor( ... , valueConstant).

And a working example:

void Main()
{
   MyClassFaker myClassFaker = new();
   MyClass class1 = myClassFaker.Get();
   MyClass class2 = myClassFaker.Get();
   class1.MyInnerClass.Id.Dump();
   class2.MyInnerClass.Id.Dump();
}

public class MyClass
{
   public int Id { get; set; }

   public MyInnerClass MyInnerClass { get; set; }
}

public class MyInnerClass
{
   public int Id { get; set; }
}

public class MyClassFaker
{
   private Bogus.Faker<MyClass> faker = new();

   public MyClassFaker()
   {
      faker
         .RuleFor(mc => mc.Id,           f => f.Random.Int())
         .RuleFor(mc => mc.MyInnerClass, f => new MyInnerClassFaker().Get()); // Your bug was here.
   }

   public MyClass Get() { return faker.Generate(); }
}

public class MyInnerClassFaker
{
   private Bogus.Faker<MyInnerClass> faker = new();

   public MyInnerClassFaker()
   {
      faker.RuleFor(mc => mc.Id, f => f.Random.Int());
   }

   public MyInnerClass Get() { return faker.Generate(); }
}

OUTPUT

1449290238
130403538

Hope that helps.

MihaiDaniel commented 2 months ago

@bchavez Indeed that was the issue, thanks for the quick response!