如何使用autofixture构建嵌套属性

use*_*935 5 c# autofixture

如何使用autofixture设置嵌套属性(它是readonly)?像这样的东西:

var result =
    fixture.Build<X>()
    .With(x => x.First.Second.Third, "value")
    .Create();
Run Code Online (Sandbox Code Playgroud)

Mar*_*ann 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; }
}
Run Code Online (Sandbox Code Playgroud)

具体地讲,我已经添加了属性Foo,Bar以及Baz向每个这些类强调的是,尽管一个可能有兴趣在设定x.First.Second.Third为特定的值,一会仍然有兴趣在具有由AutoFixture填充所有其他属性.

作为一般观察,一旦你开始使用不可变值,这就是像C#这样的语言开始揭示其局限性的地方.虽然可能,但它违背了语言的范畴.

使用不可变数据编写代码还有很多其他优点,但它在C#中变得乏味.这是我最终放弃C#并转向F#和Haskell的原因之一.虽然这有点偏离,但我提到这一点是为了明确地传达我认为使用只读属性是一个很好的设计决定,但它带来了一些已知的问题.

通常,在使用不可变值时,尤其是在测试中,最好将复制和更新方法添加到每个不可变类中,从以下开始X:

public X WithFirst(One newFirst)
{
    return new X(newFirst, this.Foo);
}
Run Code Online (Sandbox Code Playgroud)

One:

public One WithSecond(Two newSecond)
{
    return new One(newSecond, this.Bar);
}
Run Code Online (Sandbox Code Playgroud)

并在Two:

public Two WithThird(string newThird)
{
    return new Two(newThird, this.Baz);
}
Run Code Online (Sandbox Code Playgroud)

这样就可以使用FixtureGet扩展方法以产生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);
}
Run Code Online (Sandbox Code Playgroud)

这使用了Fixture.Get一个带有三个输入值的委托的重载.所有这些值由AutoFixture填充,然后你可以嵌套复制和更新方法使用x,firstsecond.

断言表明,不仅actual.First.Second.Third具有预期值,而且所有其他属性也被填充.

镜头

你可能认为你必须向AutoFixture询问firstsecond值似乎是多余的,因为它们x应该已经包含了这些.相反,您可能希望能够"进入" First.Second.Third而无需处理所有这些中间值.

这可以使用镜头.

一个镜头是与范畴论原点的结构,并且在一些编程语言(最明显的Haskell)使用.函数式编程完全是关于不可变值的,但即使是具有对不可变数据的一流支持的函数式语言,当您只需要更新单个数据时,深层嵌套的不可变记录也很笨拙.

我不打算将这个答案变成镜头教程,所以如果你真的想了解发生了什么,请用你最喜欢的函数式编程语言搜索镜头教程.

简而言之,您可以在C#中定义镜头,如下所示:

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; }
}
Run Code Online (Sandbox Code Playgroud)

镜头是一对功能.该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)));
    }
}
Run Code Online (Sandbox Code Playgroud)

Set并且Get只是使您能够获取属性的值,或者将属性设置为特定值.这里有趣的功能Compose,使您能够从组成一个镜头T,以U从镜头UV.

如果您为每个类定义了静态镜头,则效果最佳,例如X:

public static Lens<X, One> FirstLens =
    new Lens<X, One>(x => x.First, (f, x) => x.WithFirst(f));
Run Code Online (Sandbox Code Playgroud)

One:

public static Lens<One, Two> SecondLens =
    new Lens<One, Two>(o => o.Second, (s, o) => o.WithSecond(s));
Run Code Online (Sandbox Code Playgroud)

Two:

public static Lens<Two, string> ThirdLens =
    new Lens<Two, string>(t => t.Third, (s, t) => t.WithThird(s));
Run Code Online (Sandbox Code Playgroud)

这是样板代码,但是一旦掌握了它,就会很简单.即使在Haskell中它也是样板,但它可以通过Template Haskell自动化.

这使您可以使用合成镜头编写测试:

[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);
}
Run Code Online (Sandbox Code Playgroud)

你拿X.FirstLens,这是从镜头XOne先用它组成One.SecondLens,这是从镜头OneTwo.到目前为止的结果是从镜头XTwo.

由于这是一个良好Inteface,你可以继续下去,这个组合镜头Two.ThirdLens,这是从镜头Twostring.最后的合成镜头是从镜头Xstring.

然后,您可以使用Set扩展方法将此镜头设置x"ploeh".断言与上述相同,测试仍然通过.

镜头组成看起来很冗长,但这主要是C#对自定义操作员有限支持的人工制品.在Haskell中,类似的成分会从字面上看起来像first.second.third,其中first,secondthird是镜头.