【问题标题】:How to use Bogus Faker with initialization properties?如何使用带有初始化属性的 Bogus Faker?
【发布时间】:2021-06-01 06:36:50
【问题描述】:

我有一个不可变的 DTO,我想用 Bogus Faker(版本 31.0.2)伪造它,但具有覆盖规则的属性只返回构造函数初始化的内容:

DTO 示例(真实的更复杂)

using Xunit;
using Bogus;

namespace SO.Tests
{
   class ClassWithInitialization
   {
      public ClassWithInitialization(string name)
      {
         this.Name = name
      }

      public string Name { get; }
   }

DTO 伪造者示例

   class FakeClassWithInitialization : Faker<ClassWithInitialization>
   {
      private FakeClassWithInitialization() { }

      public static CreateDefault()
      {
         return (FakeClassWithInitialization) new FakeClassWithInitialization()
            .CustomInstantiator(f => new ClassWithInitialization(null))
            .RuleFor(o => o.Name, f => f.Person.FullName);
      }

      public FakeClassWithInitialization WithName(string name)
      {
         RuleFor(o => o.Name, f => name);
         return this;
      }
   }

示例测试

以下两个测试都失败了,因为 Name 属性仍然是构造函数中提供的 null。

   public class Tests
   {
      [Fact]
      public void TestClassWithInitialization()
      {
         var faker = FakeClassWithInitialization
            .CreateDefault();

         var testPoco = faker.Generate();

         Assert.False(string.IsNullOrEmpty(testPoco.Name)); #fails
      }

      [Fact]
      public void TestClassWithInitialization_with_overriding_rule()
      {
         var faker = FakeClassWithInitialization
            .CreateDefault()
            .WithName("John Smith");

         var testPoco = faker.Generate();

         Assert.AreEqual("John Smith", testPoco.Name); #fails
      }
   }
}

虽然我可以使用 Faker 为构造函数生成随机数据,但我希望能够使用这个假实例来生成替代版本,例如,上面第二个测试示例具有固定名称。

为什么这不起作用?是否有任何已知的解决方法?

注意:这和问题How can I use Bogus with private setters不一样

【问题讨论】:

    标签: c# faker bogus


    【解决方案1】:

    我刚试过这个,它似乎工作:

    class FakeClassWithInitialization : Faker<ClassWithInitialization>
    {
        private FakeClassWithInitialization() { }
    
        public static FakeClassWithInitialization CreateDefault()
        {
            return (FakeClassWithInitialization) new FakeClassWithInitialization()
                .CustomInstantiator(f => new ClassWithInitialization(f.Person.FullName));
        }
    
    }
    

    我直接将类构造函数与生成器一起使用,而不是将生成器与属性一起使用。

    我还删除了未使用的 WithName 方法。

    编辑:似乎我误解了这个问题。 我对博格斯了解不多。我以为你可以在“CreateDefault”方法中使用可选参数,但你告诉 DTO 很复杂,所以......参数太多了。

    我认为您可以使用构建器模式实现您想要的:

    public class Builder
    {
        private string _name;
    
        public Builder WithName(string name)
        {
            _name = name;
            return this;
        }
    
        public ClassWithInitialization Build()
        {
            return new Faker<ClassWithInitialization>()
                .CustomInstantiator(f =>
                    new ClassWithInitialization(
                        _name ?? f.Person.FullName
                    ))
                .Generate();
        }
    }
    
    var faker = new Builder().WithName("Hello").Build();
    var faker2 = new Builder().Build();
    

    您可以删除 FakeClassWithInitialization 并将其替换为经典的“Builder”。

    【讨论】:

    • 谢谢@Arcord - 是的,我知道这会起作用,但它对存在具有许多属性的复杂 DTO 结构的情况没有帮助。 WithName(在第二个测试示例中使用)的想法是,它采用默认的 DTO 构造并更改 DTO 的一个方面或一组相关方面。我试图使示例保持简单,但实际情况会复杂得多。当然,我可以在每个With... 方法中构造一个新的Faker,或者替换CustomInstantiator,但是在没有Faker 的情况下创建它并没有多大效果。想法?
    • 我误解了你的问题。我不知道如何使用库来解决它,但我认为经典的构建器模式可以实现您想要的。我用一个例子更新了我的答案。
    • 感谢@Arcord 的建议。我已经使用 Func 实例变量寻求类似的解决方案,然后在 .CustomInstantiator 中使用这些实例变量。我仍然更喜欢类似“Faker”的解决方案,所以我暂时不会接受您的回答,并且可能会在以后发布我的替代解决方案。再次感谢。
    【解决方案2】:

    这是可能的,但我建议不要这样做,因为该解决方案依赖于 .NET 的反射。

    有一个new Faker&lt;T&gt;(binder:...) binder 构造函数参数。 IBinder 接口是 Faker&lt;T&gt; 用来反映 T 以发现可设置的属性和字段的接口。无论IBinder.GetMembers(Type t) 返回什么都是Faker&lt;&gt;T 中看到的。

    有了这些信息,让我们看看编译器如何生成一个带有public参数化构造函数和只读属性的对象:

    public class Foo
    {
       public Foo(string name){
          this.Name = name;
       }
       public string Name { get; }
    }
    

    C# 编译器生成:

    public class Foo
    {
        // Fields
        [CompilerGenerated, DebuggerBrowsable((DebuggerBrowsableState) DebuggerBrowsableState.Never)]
        private readonly string <Name>k__BackingField;
    
        // Methods
        public Foo(string name)
        {
            this.<Name>k__BackingField = name;
        }
    
        // Properties
        public string Name => this.<Name>k__BackingField;
    }
    

    Foo.Name 属性的存储使用名为 Foo.&lt;Name&gt;k__BackingField 的支持字段。这个支持字段是我们需要IBinder 提升到Faker&lt;&gt; 的内容。以下BackingFieldBinder : IBinder 执行此操作:

    public class BackingFieldBinder : IBinder
    {
       public Dictionary<string, MemberInfo> GetMembers(Type t)
       {
          var availableFieldsForFakerT = new Dictionary<string, MemberInfo>();
          var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
          var allMembers = t.GetMembers(bindingFlags);     
          var allBackingFields = allMembers
                                  .OfType<FieldInfo>()
                                  .Where(fi => fi.IsPrivate && fi.IsInitOnly)
                                  .Where(fi => fi.Name.EndsWith("__BackingField"))
                                  .ToList();
          
          foreach( var backingField in allBackingFields){
             var fieldName = backingField.Name.Substring(1).Replace(">k__BackingField","");
             availableFieldsForFakerT.Add(fieldName, backingField);
          }
          return availableFieldsForFakerT;
       }
    }
    

    自定义上面的GetMembers() 方法以满足您的需求。如果您还想包含public 的字段或T 的属性,则需要更改代码。

    我们必须解决的最后一个问题是在不指定构造函数参数的情况下创建对象。我们可以通过使用来自FormatterServicesRuntimeHelpers.GetUninitializedObject() 来做到这一点。为此,我们将创建一个扩展方法来扩展Faker&lt;T&gt; API,如下所示:

    public static class MyExtensionsForFakerT
    {
       public static Faker<T> SkipConstructor<T>(this Faker<T> fakerOfT) where T : class
       {
          return fakerOfT.CustomInstantiator( _ => FormatterServices.GetUninitializedObject(typeof(T)) as T);
       }
    }
    

    有了这两个组件,我们终于可以编写如下代码了:

    void Main()
    {
       var backingFieldBinder = new BackingFieldBinder();
       var fooFaker = new Faker<Foo>(binder: backingFieldBinder)
                          .SkipConstructor()
                          .RuleFor(f => f.Name, f => f.Name.FullName());
                          
       var foo = fooFaker.Generate();
       foo.Dump();
    }
    
    public class Foo
    {
       public Foo(string name)
       {
          this.Name = name;
       }
       public string Name {get;}
    }
    

    您可以找到完整的工作示例here。此外,您可能会在Issue 213 中找到其他有用的解决方案。

    【讨论】:

      猜你喜欢
      • 2017-06-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-08
      • 1970-01-01
      • 1970-01-01
      • 2017-01-09
      • 2014-04-24
      相关资源
      最近更新 更多