AutoFixture mixing PropertyData with multiple entries and AutoData (using AutoMoqCustomization)

左心房为你撑大大i 提交于 2019-12-23 03:15:09

问题


I've looked at both of these similar SO questions:

  • AutoFixture: PropertyData and heterogeneous parameters
  • AutoFixture CompositeDataAttribute does not work with PropertyDataAttribute

And they're awesome and get me nearly there. But both examples use only one entry in the emitted IEnumerable PropertyData (i.e.: yield return new object[] { 2, 4 }; -- see: https://stackoverflow.com/a/16843837/201308) This works, but it blows up whenever I want to do test over more than one object[] test data. I have a whole collection of test data I want to send.

I'm thinking the answer here (https://stackoverflow.com/a/19309577/201308) is similar to what I need, but I can't figure it out. I basically need AutoFixture to create a sut instance for each iteration of the PropertyData.

Some reference:

public static IEnumerable<object[]> TestData
{
    get
    {
        // totally doesn't work
        return new List<object[]>()
        {
            new object[] { new MsgData() { Code = "1" }, CustomEnum.Value1 },
            new object[] { new MsgData() { Code = "2" }, CustomEnum.Value2 },
            new object[] { new MsgData() { Code = "3" }, CustomEnum.Value3 },
            new object[] { new MsgData() { Code = "4" }, CustomEnum.Value4 },
        };

        // totally works
        //yield return new object[] { new MsgData() { Code = "1" }, CustomEnum.Value1 };
    }
}

Returning the list results in a "Expected 3 parameters, got 2 parameters" exception. If I just return the single yield statement, it works. (I've also tried looping over the list and yielding each item -- no difference, which makes sense, seeing how it's pretty much the exact same thing as returning the full list.)

xUnit test method:

[Theory]
[AutoMoqPropertyData("TestData")]
public void ShouldMapEnum(MsgData msgData, CustomEnum expectedEnum, SomeObject sut)
{
    var customEnum = sut.GetEnum(msgData);
    Assert.Equal(expectedEnum, customEnum);
}

AutoMoqPropertyData implementation:

public class AutoMoqPropertyDataAttribute : CompositeDataAttribute
{
    public AutoMoqPropertyDataAttribute(string dataProperty)
        : base(new DataAttribute[]
            {
                new PropertyDataAttribute(dataProperty),
                new AutoDataAttribute(new Fixture().Customize(new AutoMoqCustomization())) 
            })
    { }
}

What am I missing? Can I mix both PropertyData- and AutoData-driven AutoFixture attributes like this when wanting multiple iterations of the PropertyData data?

EDIT Here's the exception stack trace:

System.InvalidOperationException: Expected 3 parameters, got 2 parameters
    at Ploeh.AutoFixture.Xunit.CompositeDataAttribute.<GetData>d__0.MoveNext()
    at Xunit.Extensions.TheoryAttribute.<GetData>d__7.MoveNext()
    at Xunit.Extensions.TheoryAttribute.EnumerateTestCommands(IMethodInfo method)
Result StackTrace:  
    at Xunit.Extensions.TheoryAttribute.<>c__DisplayClass5.<EnumerateTestCommands>b__1()
    at Xunit.Extensions.TheoryAttribute.LambdaTestCommand.Execute(Object testClass)

回答1:


You have to supply the test cases as described in this answer that Ruben Bartelink points out.

[Theory]
[AutoMoqPropertyData("Case1")]
[AutoMoqPropertyData("Case2")]
[AutoMoqPropertyData("Case3")]
[AutoMoqPropertyData("Case4")]
public void ShouldMapEnum(
    MsgData msgData, CustomEnum expectedEnum, SomeObject sut)
{
    var customEnum = sut.GetEnum(msgData);
    Assert.Equal(expectedEnum, customEnum);
}

public static IEnumerable<object[]> Case1 { get {
    yield return new object[] { 
        new MsgData { Code = "1" }, CustomEnum.Value1 }; } }

public static IEnumerable<object[]> Case2 { get {
    yield return new object[] { 
        new MsgData { Code = "2" }, CustomEnum.Value2 }; } }

public static IEnumerable<object[]> Case3 { get {
    yield return new object[] { 
        new MsgData { Code = "3" }, CustomEnum.Value3 }; } }

public static IEnumerable<object[]> Case4 { get {
    yield return new object[] { 
        new MsgData { Code = "4" }, CustomEnum.Value4 }; } }

However, the problem tends to be more generic (rather than specific) because of:

  1. the way xUnit.net models parameterized tests via non-generic, untyped, arrays
  2. the attribute-based model which really makes these test cases look like second-class citizens
  3. the noise by the language with all these type declarations and curly brackets

For 1. and 2. and the existing xUnit.net model for parameterized tests there is not much left to do.


For 3. if the code is written in F# most of type declaration noise (and a few curly brackets) go away:

let Case1 : seq<obj[]> = seq {
    yield [| { Code = "1" }; Value1 |] }

let Case2 : seq<obj[]> = seq {
    yield [| { Code = "2" }; Value2 |] }

let Case3 : seq<obj[]> = seq {
    yield [| { Code = "3" }; Value3 |] }

let Case4 : seq<obj[]> = seq {
    yield [| { Code = "4" }; Value4 |] }

[<Theory>]
[<AutoMoqPropertyData("Case1")>]
[<AutoMoqPropertyData("Case2")>]
[<AutoMoqPropertyData("Case3")>]
[<AutoMoqPropertyData("Case4")>]
let ShouldMapEnum (msgData, expected, sut : SomeObject) =
    let actual = sut.GetEnum(msgData)
    Assert.Equal(expected, actual.Value)

Below are the types used to pass the test:

type MsgData = { Code : string }

[<AutoOpen>]
type Custom = Value1 | Value2 | Value3 | Value4

type SomeObject () =
    member this.GetEnum msgData = 
        match msgData.Code with 
        | "1" -> Some(Value1)
        | "2" -> Some(Value2)
        | "3" -> Some(Value3)
        | "4" -> Some(Value4)
        | _   -> None

[<AttributeUsage(AttributeTargets.Field, AllowMultiple = true)>]
type AutoMoqPropertyDataAttribute (dataProperty) =
    inherit CompositeDataAttribute(
        PropertyDataAttribute(dataProperty), 
        AutoDataAttribute())



回答2:


I needed this myself and I wrote a new class PropertyAutoData which combines PropertyData and AutoFixture similar to how InlineAutoData combines InlineData and AutoFixture. The usage is:

[Theory]
[PropertyAutoData("ColorPairs")]
public void ReverseColors([TestCaseParameter] TestData testData, int autoGenValue) { ... }

public static IEnumerable<object[]> ColorPairs
{
  get
  {
    yield return new object[] { new TestData { Input = Color.Black, Expected = Color.White } };
    yield return new object[] { new TestData { Input = Color.White, Expected = Color.Black } };
  }
}

Note the [TestCaseParameter] attribute on param testData. This indicates that the parameter value will be supplied from the property. It needs to be explicitly specified because AutoFixture kind of changed the meaning of parametrized tests.

Running this yields 2 tests as expected in which the autoGenValue has the same auto-generated value. You can change this behavior by setting the Scope of auto-generated data:

[PropertyAutoData("ColorPairs", Scope = AutoDataScope.Test)] // default is TestCase

You can also use this together with SubSpec's Thesis:

[Thesis]
[PropertyAutoData("ColorPairs")]
public void ReverseColors([TestCaseParameter] TestData testData, int autoGenValue)

To use this with Moq you need to extend it, i.e.

public class PropertyMockAutoDataAttribute : PropertyAutoDataAttribute
{
    public PropertyFakeAutoDataAttribute(string propertyName)
        : base(propertyName, new Fixture().Customize(new AutoMoqCustomization()))
    {
    }
}

Here's the code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Ploeh.AutoFixture.Xunit;
using Xunit.Extensions;

/// <summary>
/// Provides a data source for a data theory, with the data coming from a public static property on the test class combined with auto-generated data specimens generated by AutoFixture.
/// </summary>
public class PropertyAutoDataAttribute : AutoDataAttribute
{
    private readonly string _propertyName;

    public PropertyAutoDataAttribute(string propertyName)
    {
        _propertyName = propertyName;
    }

    public PropertyAutoDataAttribute(string propertyName, IFixture fixture)
        : base(fixture)
    {
        _propertyName = propertyName;
    }

    /// <summary>
    /// Gets or sets the scope of auto-generated data.
    /// </summary>
    public AutoDataScope Scope { get; set; }

    public override IEnumerable<object[]> GetData(MethodInfo methodUnderTest, Type[] parameterTypes)
    {
        var parameters = methodUnderTest.GetParameters();
        var testCaseParametersIndices = GetTestCaseParameterIndices(parameters);
        if (!testCaseParametersIndices.Any())
        {
            throw new InvalidOperationException(string.Format("There are no parameters marked using {0}.", typeof(TestCaseParameterAttribute).Name));
        }
        if (testCaseParametersIndices.Length == parameters.Length)
        {
            throw new InvalidOperationException(string.Format("All parameters are provided by the property. Do not use {0} unless there are other parameters that AutoFixture should provide.", typeof(PropertyDataAttribute).Name));
        }

        // 'split' the method under test in 2 methods: one to get the test case data sets and another one to get the auto-generated data set
        var testCaseParameterTypes = parameterTypes.Where((t, i) => testCaseParametersIndices.Contains(i)).ToArray();
        var testCaseMethod = CreateDynamicMethod(methodUnderTest.Name + "_TestCase", testCaseParameterTypes);
        var autoFixtureParameterTypes = parameterTypes.Where((t, i) => !testCaseParametersIndices.Contains(i)).ToArray();
        var autoFixtureTestMethod = CreateDynamicMethod(methodUnderTest.Name + "_AutoFixture", autoFixtureParameterTypes);

        // merge the test case data and the auto-generated data into a new array and yield it
        // the merge depends on the Scope:
        // * if the scope is TestCase then auto-generate data once for all tests
        // * if the scope is Test then auto-generate data for every test

        var testCaseDataSets = GetTestCaseDataSets(methodUnderTest.DeclaringType, testCaseMethod, testCaseParameterTypes);
        object[] autoGeneratedDataSet = null;
        if (Scope == AutoDataScope.TestCase)
        {
            autoGeneratedDataSet = GetAutoGeneratedData(autoFixtureTestMethod, autoFixtureParameterTypes);
        }
        var autoFixtureParameterIndices = Enumerable.Range(0, parameters.Length).Except(testCaseParametersIndices).ToArray();
        foreach (var testCaseDataSet in testCaseDataSets)
        {
            if (testCaseDataSet.Length != testCaseParameterTypes.Length)
            {
                throw new ApplicationException("There is a mismatch between the values generated by the property and the test case parameters.");
            }

            var mergedDataSet = new object[parameters.Length];
            CopyAtIndices(testCaseDataSet, mergedDataSet, testCaseParametersIndices);

            if (Scope == AutoDataScope.Test)
            {
                autoGeneratedDataSet = GetAutoGeneratedData(autoFixtureTestMethod, autoFixtureParameterTypes);
            }
            CopyAtIndices(autoGeneratedDataSet, mergedDataSet, autoFixtureParameterIndices);
            yield return mergedDataSet;
        }
    }

    private static int[] GetTestCaseParameterIndices(ParameterInfo[] parameters)
    {
        var testCaseParametersIndices = new List<int>();
        for (var index = 0; index < parameters.Length; index++)
        {
            var parameter = parameters[index];
            var isTestCaseParameter = parameter.GetCustomAttributes(typeof(TestCaseParameterAttribute), false).Length > 0;
            if (isTestCaseParameter)
            {
                testCaseParametersIndices.Add(index);
            }
        }
        return testCaseParametersIndices.ToArray();
    }

    private static MethodInfo CreateDynamicMethod(string name, Type[] parameterTypes)
    {
        var method = new DynamicMethod(name, typeof(void), parameterTypes);
        return method.GetBaseDefinition();
    }

    private object[] GetAutoGeneratedData(MethodInfo method, Type[] parameterTypes)
    {
        var autoDataSets = base.GetData(method, parameterTypes).ToArray();
        if (autoDataSets == null || autoDataSets.Length == 0)
        {
            throw new ApplicationException("There was no data automatically generated by AutoFixture");
        }
        if (autoDataSets.Length != 1)
        {
            throw new ApplicationException("Multiple sets of data were automatically generated. Only one was expected.");
        }
        return autoDataSets.Single();
    }

    private IEnumerable<object[]> GetTestCaseDataSets(Type testClassType, MethodInfo method, Type[] parameterTypes)
    {
        var attribute = new PropertyDataAttribute(_propertyName) { PropertyType = testClassType };
        return attribute.GetData(method, parameterTypes);
    }

    private static void CopyAtIndices(object[] source, object[] target, int[] indices)
    {
        var sourceIndex = 0;
        foreach (var index in indices)
        {
            target[index] = source[sourceIndex++];
        }
    }
}

/// <summary>
/// Defines the scope of auto-generated data in a theory.
/// </summary>
public enum AutoDataScope
{
    /// <summary>
    /// Data is auto-generated only once for all tests.
    /// </summary>
    TestCase,
    /// <summary>
    /// Data is auto-generated for every test.
    /// </summary>
    Test
}

/// <summary>
/// Indicates that the parameter is part of a test case rather than being auto-generated by AutoFixture.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public class TestCaseParameterAttribute : Attribute
{
}


来源:https://stackoverflow.com/questions/22179928/autofixture-mixing-propertydata-with-multiple-entries-and-autodata-using-automo

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!