如何使用autofixture设置嵌套属性(只读)?像这样:
var result =
fixture.Build<X>()
.With(x => x.First.Second.Third, "value")
.Create();
答案 0 :(得分:10)
如果我正确理解了这个问题,我会假设我们有这样的课程:
public class X
{
public X(One first, string foo)
{
First = first;
Foo = foo;
}
public One First { get; }
public string Foo { get; }
}
public class One
{
public One(Two second, int bar)
{
Second = second;
Bar = bar;
}
public Two Second { get; }
public int Bar { get; }
}
public class Two
{
public Two(string third, bool baz)
{
Third = third;
Baz = baz;
}
public string Third { get; }
public bool Baz { get; }
}
具体来说,我已将属性Foo
,Bar
和Baz
添加到每个类中,以强调虽然有人可能有兴趣设置{{1}对于一个特定的值,人们仍然会对AutoFixture填充所有其他属性感兴趣。
作为一般观察,一旦你开始使用不可变值,这就是像C#这样的语言开始揭示其局限性的地方。虽然可能,但它违背了语言。
使用不可变数据编写代码还有很多其他优点,但在C#中却很繁琐。这是我最终放弃C#并转移到F#和Haskell的原因之一。虽然这有点偏离,但我提到这一点是为了明确地传达我认为使用只读属性是一个很好的设计决定,但它带来了一些已知的问题。
通常,在使用不可变值时,特别是在测试中,从x.First.Second.Third
开始,每个不可变类的add copy-and-update methods都是个好主意:
X
在public X WithFirst(One newFirst)
{
return new X(newFirst, this.Foo);
}
:
One
和public One WithSecond(Two newSecond)
{
return new One(newSecond, this.Bar);
}
:
Two
这使您可以使用public Two WithThird(string newThird)
{
return new Two(newThird, this.Baz);
}
的{{1}}扩展方法生成具有特定Fixture
值的Get
值,但所有其他值均为由AutoFixture自由填充。
以下测试通过:
X
这使用了First.Second.Third
的重载,它带有一个具有三个输入值的委托。所有这些值都由AutoFixture填充,然后您可以使用[Fact]
public void BuildWithThird()
{
var fixture = new Fixture();
var actual =
fixture.Get((X x, One first, Two second) =>
x.WithFirst(first.WithSecond(second.WithThird("ploeh"))));
Assert.Equal("ploeh", actual.First.Second.Third);
Assert.NotNull(actual.Foo);
Assert.NotEqual(default(int), actual.First.Bar);
Assert.NotEqual(default(bool), actual.First.Second.Baz);
}
,Fixture.Get
和x
嵌套复制和更新方法。
断言表明first
不仅具有预期值,而且还填充了所有其他属性。
您可能认为,由于second
和actual.First.Second.Third
值已经包含,因此您需要向AutoFixture询问first
和second
值似乎是多余的。相反,您可能希望能够进入&#39; x
,无需处理所有这些中间价值。
这可以使用镜头。
镜头是一种源于类别理论的构造,并且在一些编程语言中使用(最值得注意的是Haskell)。函数式编程完全是关于不可变值的,但即使是对不可变数据具有一流支持的函数式语言,当您只需要更新单个数据时,深层嵌套的不可变记录也很笨拙。
我不打算将这个答案变成镜头教程,所以如果你真的想了解发生了什么,请用你最喜欢的函数编程语言搜索镜头教程。
简而言之,您可以使用C#定义镜头:
First.Second.Third
镜头是一对功能。 public class Lens<T, V>
{
public Lens(Func<T, V> getter, Func<V, T, T> setter)
{
Getter = getter;
Setter = setter;
}
internal Func<T, V> Getter { get; }
internal Func<V, T, T> Setter { get; }
}
返回属性的值,给定一个完整的&#39;宾语。 Getter
是一个函数,它接受一个值和一个旧对象,并返回一个新的对象,其属性已更改为该值。
您可以定义一组对镜头进行操作的功能:
Setter
public static class Lens
{
public static V Get<T, V>(this Lens<T, V> lens, T item)
{
return lens.Getter(item);
}
public static T Set<T, V>(this Lens<T, V> lens, T item, V value)
{
return lens.Setter(value, item);
}
public static Lens<T, V> Compose<T, U, V>(
this Lens<T, U> lens1,
Lens<U, V> lens2)
{
return new Lens<T, V>(
x => lens2.Get(lens1.Get(x)),
(v, x) => lens1.Set(x, lens2.Set(lens1.Get(x), v)));
}
}
和Set
只是让您获取属性的值,或将属性设置为特定值。这里有趣的功能是Get
,您可以使用Compose
到T
的镜头从U
到U
合成镜头。
如果您为每个班级定义了静态镜头,则效果最佳,例如V
:
X
public static Lens<X, One> FirstLens =
new Lens<X, One>(x => x.First, (f, x) => x.WithFirst(f));
:
One
public static Lens<One, Two> SecondLens =
new Lens<One, Two>(o => o.Second, (s, o) => o.WithSecond(s));
:
Two
这是样板代码,但是一旦掌握了它,它就会很简单。即使在Haskell它的样板文件中,它也可以通过Template Haskell自动化。
这使您可以使用合成镜头编写测试:
public static Lens<Two, string> ThirdLens =
new Lens<Two, string>(t => t.Third, (s, t) => t.WithThird(s));
你拍摄[Fact]
public void BuildWithLenses()
{
var fixture = new Fixture();
var actual = fixture.Get((X x) =>
X.FirstLens.Compose(One.SecondLens).Compose(Two.ThirdLens).Set(x, "ploeh"));
Assert.Equal("ploeh", actual.First.Second.Third);
Assert.NotNull(actual.Foo);
Assert.NotEqual(default(int), actual.First.Bar);
Assert.NotEqual(default(bool), actual.First.Second.Baz);
}
,这是X.FirstLens
到X
的镜头,首先用One
构成,这是从One.SecondLens
到{One
的镜头{1}}。到目前为止,结果是从Two
到X
的镜头。
由于这是一个Fluent Inteface,您可以继续使用Two
来构建此镜头,Two.ThirdLens
是Two
到string
的镜头。最终合成的镜头是X
到string
的镜头。
然后,您可以使用Set
扩展程序将此镜头设置为x
至"ploeh"
。断言与上述相同,测试仍然通过。
镜头组成看起来很冗长,但这主要是C#的人工制品对定制运营商的有限支持。在Haskell中,类似的组合看起来像first.second.third
,其中first
,second
和third
是镜头。