2

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

4

2 回答 2

2

这是可能的,但我建议不要这样做,因为该解决方案依赖于 .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()方法以满足您的需求。如果您也想包含public字段或属性,则需要更改代码T

我们必须解决的最后一个问题是在不指定构造函数参数的情况下创建对象。我们可以通过使用.GetUninitializedObject()fromFormatterServices或来做到这一点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;}
}

结果

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

于 2021-03-06T05:30:28.297 回答
0

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

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

于 2021-03-02T17:37:54.203 回答