5

私はAutoFixtureが大好きですが、非常に反復的な「アレンジ」コードに遭遇しました。これは、どういうわけか、処理できるはずだと感じています。

これが私のシナリオで、CastleDynamicProxyの実装を使用して示されてIInterceptorます

最初にテスト対象のシステム:

public class InterceptorA : IInterceptor
{
    public void Intercept(IInvocation context)
    {
        object proxy = context.Proxy;
        object returnValue = context.ReturnValue;
        // Do something with proxy and returnValue
    }
}

public class InterceptorB : IInterceptor
{
    public void Intercept(IInvocation context)
    {
        object returnValue = context.ReturnValue;
        // Do something with different returnValue
    }
}

ここで、 xUnitのデータ理論サポートを活用するいくつかの簡単なテストについて説明します。

public class InterceptorATests
{
    [Theory, CustomAutoData]
    public void TestA1(InterceptorA sut, IInvocation context)
    {
        Mock.Get(context).Setup(c => c.Proxy).Returns("a");
        Mock.Get(context).Setup(c => c.ReturnValue).Returns("b");

        sut.Intercept(context);
        // assert
    }
}

public class InterceptorBTests
{
    [Theory, CustomAutoData]
    public void TestB1(InterceptorB sut, IInvocation context)
    {
        Mock.Get(context).Setup(c => c.ReturnValue).Returns("z");
        sut.Intercept(context);
        // assert
    }
}

私のCustomAutoData属性は実際にAutoFixtureをカスタマイズして、の挿入されたインスタンスIInvocationほとんど適切に構成されるようにしますが、すべての実装はプロパティとプロパティにIInterceptor完全に異なるタイプを期待するため、各テストはそれらを独自に設定する必要があります。(したがって、呼び出し。)ProxyReturnValueMock.Get(context).Setup(...)

これは問題ありませんが、のすべてのテストは、InterceptorATestsのすべてのテストと同様に、同じ数行の配置を繰り返す必要がありInterceptorBTestsます。

Mock.Get(...)繰り返しの呼び出しをきれいに削除する方法はありますか?IFixture特定のテストクラスのインスタンスにアクセスするための良い方法はありますか?

4

2 に答える 2

7

本当にテストしたいことが正確にであるかに応じて、実行できることはたくさんあります。

まず、この特定の質問の問題の多くは、IInvocationの非常に弱い型のAPIに起因していることと、Moqが通常プロパティを実装しているようにプロパティを実装していないことを指摘しておきます。

スタブが必要ない場合はセットアップしないでください

まず、必要がない場合は、ProxyプロパティとReturnValueプロパティの戻り値を設定する必要はありません

AutoFixture.AutoMoqがMock<T>インスタンスを設定する方法は、常にを設定することDefaultValue = DefaultValue.Mockです。両方のプロパティの戻り型はデフォルトのコンストラクターobjectobjectあり、デフォルトのコンストラクターがあるため、オブジェクト(実際には)が自動的にObjectProxy返されます。

つまり、これらのテストも合格します。

[Theory, CustomAutoData]
public void TestA2(InterceptorA sut, IInvocation context)
{
    sut.Intercept(context);
    // assert
}

[Theory, CustomAutoData]
public void TestB2(InterceptorB sut, IInvocation context)
{
    sut.Intercept(context);
    // assert
}

ReturnValueを直接割り当てます

私の答えの残りの部分では、テストでプロパティ値を実際に割り当てたり、読み取ったりする必要があると想定します。

まず、ReturnValueを直接割り当てることで、重いMoq構文を減らすことができます。

[Theory, Custom3AutoData]
public void TestA3(InterceptorA sut, IInvocation context)
{
    context.ReturnValue = "b";

    sut.Intercept(context);
    // assert
    Assert.Equal("b", context.ReturnValue);
}

[Theory, Custom3AutoData]
public void TestB3(InterceptorB sut, IInvocation context)
{
    context.ReturnValue = "z";

    sut.Intercept(context);
    // assert
    Assert.Equal("z", context.ReturnValue);
}

ただし、ReturnValue書き込み可能なプロパティであるため、この場合にのみ機能します。Proxy読み取り専用であるため(コンパイルされないため)、プロパティでは機能しません。

これを機能させるには、IInvocationプロパティを「実際の」プロパティとして扱うようにMoqに指示する必要があります。

public class Customization3 : CompositeCustomization
{
    public Customization3()
        : base(
            new RealPropertiesOnInvocation(),
            new AutoMoqCustomization())
    {
    }

    private class RealPropertiesOnInvocation : ICustomization
    {
        public void Customize(IFixture fixture)
        {
            fixture.Register<Mock<IInvocation>>(() =>
                {
                    var td = new Mock<IInvocation>();
                    td.DefaultValue = DefaultValue.Mock;
                    td.SetupAllProperties();
                    return td;
                });
        }
    }
}

への呼び出しに注意してくださいSetupAllProperties

これが機能するのは、AutoFixture.AutoMoqが、インターフェイスのすべての要求をそのインターフェイスのモックの要求に中継することによって機能するためです。つまり、の要求IInvocationはの要求に変換されMock<IInvocation>ます。

テスト値を設定しないでください。それらを読み返します

最後に、自分自身に問いかける必要があります。これらのプロパティに特定の値( "a"、 "b"、 "z"など)を割り当てる必要が本当にありますか。AutoFixtureに必要な値を作成させることはできませんか?その場合、明示的に割り当てる必要がありますか?代わりに、割り当てられた値を読み戻すことはできませんか?

これはおそらく、私がシグナルタイプと呼ぶちょっとしたトリックです。シグナルタイプは、値の特定の役割を通知するクラスです。

各プロパティの信号タイプを導入します。

public class InvocationReturnValue
{
    private readonly object value;

    public InvocationReturnValue(object value)
    {
        this.value = value;
    }

    public object Value
    {
        get { return this.value; }
    }
}

public class InvocationProxy
{
    private readonly object value;

    public InvocationProxy(object value)
    {
        this.value = value;
    }

    public object Value
    {
        get { return this.value; }
    }
}

(値を常に文字列にする必要がある場合は、コンストラクターの署名を変更して、のstring代わりにを要求することができますobject。)

IInvocationインスタンスが構成されているときに同じインスタンスが再利用されることがわかるように、気になるシグナルタイプをフリーズします。

[Theory, Custom4AutoData]
public void TestA4(
    InterceptorA sut,
    [Frozen]InvocationProxy proxy,
    [Frozen]InvocationReturnValue returnValue,
    IInvocation context)
{
    sut.Intercept(context);
    // assert
    Assert.Equal(proxy.Value, context.Proxy);
    Assert.Equal(returnValue.Value, context.ReturnValue);
}

[Theory, Custom4AutoData]
public void TestB4(
    InterceptorB sut,
    [Frozen]InvocationReturnValue returnValue,
    IInvocation context)
{
    sut.Intercept(context);
    // assert
    Assert.Equal(returnValue.Value, context.ReturnValue);
}

ReturnValueこのアプローチの利点は、またはを気にしないテストケースでは、Proxyこれらのメソッド引数を省略できることです。

対応するカスタマイズは、前のカスタマイズの拡張です。

public class Customization4 : CompositeCustomization
{
    public Customization4()
        : base(
            new RelayedPropertiesOnInvocation(),
            new AutoMoqCustomization())
    {
    }

    private class RelayedPropertiesOnInvocation : ICustomization
    {
        public void Customize(IFixture fixture)
        {
            fixture.Register<Mock<IInvocation>>(() =>
                {
                    var td = new Mock<IInvocation>();
                    td.DefaultValue = DefaultValue.Mock;
                    td.SetupAllProperties();

                    td.Object.ReturnValue = 
                        fixture.CreateAnonymous<InvocationReturnValue>().Value;
                    td.Setup(i => i.Proxy).Returns(
                        fixture.CreateAnonymous<InvocationProxy>().Value);

                    return td;
                });
        }
    }
}

各プロパティの値は、IFixtureインスタンスに対応するシグナルタイプの新しいインスタンスを作成するように要求し、その値をアンラップすることによって割り当てられることに注意してください。

このアプローチは一般化することができますが、それがその要点です。

于 2012-12-19T22:46:38.107 に答える
0

私はこの問題を解決するためにxUnitの拡張性ポイントにレベルを下げることになりました-マークの答えで言及された信号タイプパターンに触発されました。

これで、私のテストには追加の属性がありますSignal

public class InterceptorATests
{
    [Theory, CustomAutoData]
    public void TestA1(InterceptorA sut, [Signal(typeof(SpecialContext))] IInvocation context)
    {
        // no more repetitive arrangement!
        sut.Intercept(context);
        // assert
    }
}

SignalAttributeクラスは非常に単純です。

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class SignalAttribute : Attribute
{
    public ISignalType SignalType { get; set; }

    public SignalAttribute(Type customization)
    {
        SignalType = (ISignalType)Activator.CreateInstance(customization);
    }
}

本当の魔法は、新しく更新されたCustomAutoDataクラスにあります。

public class CustomAutoDataAttribute: AutoDataAttribute
{
    public CustomAutoDataAttribute() : base(new Fixture().Customize(new AutoMoqCustomization()))
    {
    }

    public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
    {
        Type input = null;
        ISignalType signalType = null;

        foreach (var parameter in methodUnderTest.GetParameters())
        {
            var attribute = parameter.GetCustomAttribute(typeof(SignalAttribute)) as SignalAttribute;

            if (attribute == null)
                continue;

            input = parameter.ParameterType;
            signalType = attribute.SignalType;

            break;
            // this proof of concept only supports one parameter at a time
        }

        var result = base.GetData(methodUnderTest, parameterTypes);

        if (input == null)
            return result;

        int index = Array.IndexOf(parameterTypes, input);

        foreach (var objectSet in result)
        {
            signalType.Customize(objectSet[index]);
        }

        return result;
    }
}

最後に、を作成しますSpecialContext。でネストされたクラスとして作成しましたInterceptorATestsが、どこにでも存在できます。

public class SpecialContext : ISignalType
{
    public void Customize(object obj)
    {
        var input = (IInvocation)obj;
        Mock.Get(input).Setup(i => i.Proxy).Returns("a");
        Mock.Get(input).Setup(i => i.ReturnValue).Returns("b");
    }
}

これにより、AutoFixtureが作成するほとんどIInvocationの作業を完了した後で効果的に接続できますが、1か所でさらにカスタマイズを指定できます。

注:これは概念実証コードです!多くのシナリオを適切に処理しません。自己責任。

于 2012-12-21T19:37:54.610 に答える