问题描述
我有一个不可变的 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()
方法以满足您的需求。如果您还想包含 public
的 T
字段或属性,则需要更改代码。
我们必须解决的最后一个问题是在不指定构造函数参数的情况下创建对象。我们可以通过使用 .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;}
}
您可以找到完整的工作示例 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”。