AutoFixture将PropertyData与多个条目和AutoData混合(使用AutoMoqCustomization)

时间:2014-03-04 18:24:39

标签: c# moq xunit xunit.net autofixture

我看过这两个类似的SO问题:

他们很棒,让我差不多到了那里。但是这两个示例在发出的IEnumerable PropertyData中只使用了一个条目(即:yield return new object[] { 2, 4 }; - 请参阅:https://stackoverflow.com/a/16843837/201308)这有效,但每当我想对多个对象进行测试时它会爆炸[]测试数据。我有一整套想要发送的测试数据。

我在想这里的答案(https://stackoverflow.com/a/19309577/201308)与我的需求类似,但我无法弄清楚。我基本上需要AutoFixture为PropertyData的每次迭代创建一个sut实例。

一些参考:

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 };
    }
}

返回列表会产生“预期的3个参数,得到2个参数”异常。如果我只返回单个yield语句,它就可以了。 (我也尝试在列表上循环并产生每个项目 - 没有区别,这是有道理的,看看它与返回完整列表几乎完全相同。)

xUnit测试方法:

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

AutoMoqPropertyData实施:

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

我错过了什么?想要多次迭代PropertyData数据时,我可以混合使用PropertyData和AutoData驱动的AutoFixture属性吗?

修改 这是异常堆栈跟踪:

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)

2 个答案:

答案 0 :(得分:1)

您必须按照this answer 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 }; } }

然而,问题往往更通用(而非具体),因为:

  1. xUnit.net通过非泛型,无类型数组
  2. 对参数化测试进行建模的方式
  3. 基于属性的模型,它真正使这些测试用例看起来像二等公民
  4. 所有这些类型声明和大括号的语言噪音
  5. 对于1.2.以及参数化测试的现有xUnit.net模型,没有什么可做的。


    对于3.如果代码是用F#编写的,大多数类型声明噪声(以及一些大括号)都会消失:

    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)
    

    以下是用于通过测试的类型:

    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())
    

答案 1 :(得分:0)

我自己需要这个,我写了一个新的PropertyAutoData课程,它结合了PropertyData和AutoFixture,类似于InlineAutoData结合InlineData和AutoFixture的方式。用法是:

[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 } };
  }
}

请注意param [TestCaseParameter]上的testData属性。这表示将从属性提供参数值。它需要明确指定,因为AutoFixture类改变了参数化测试的含义。

运行此操作会产生2个预期的测试,其中autoGenValue具有相同的自动生成的值。您可以通过设置自动生成数据的Scope来更改此行为:

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

您也可以将其与SubSpec Thesis

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

要在Moq中使用它,你需要扩展它,即

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

以下是代码:

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
{
}