如何使用具有初始化属性的 Bogus Faker? 示例 DTO真实的更复杂示例 DTO Faker示例测试

问题描述

我有一个不可变的 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 Faker

   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 属性在构造函数中仍然为空。

   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

不同

解决方法

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

有一个 new Faker<T>(binder:...) 绑定器构造函数参数。 IBinder 接口是 Faker<T> 用来反映 T 以发现可设置的属性和字段的接口。 IBinder.GetMembers(Type t) 返回的是 Faker<>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.<Name>k__BackingField 的支持字段。这个支持字段是我们需要 IBinder 提升到 Faker<> 的。以下 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() 方法以满足您的需求。如果您还想包含 publicT 字段或属性,则需要更改代码。

我们必须解决的最后一个问题是在不指定构造函数参数的情况下创建对象。我们可以通过使用 .GetUninitializedObject()FormatterServices 中的 RuntimeHelpers 来做到这一点。为此,我们将创建一个扩展方法来扩展 Faker<T> 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;}
}

Results

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

,

我刚刚试过这个,它似乎有效:

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”。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...