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

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

  • AutoFixture:PropertyData和异构参数
  • AutoFixture CompositeDataAttribute不适用于PropertyDataAttribute

他们很棒,让我差不多到了那里。 但是这两个示例在发出的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 TestData { get { // totally doesn't work return new List() { 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个参数”exception。 如果我只返回单个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属性吗?

编辑这是exception堆栈跟踪:

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

您必须按照Ruben Bartelink 指出的 答案中所述提供测试用例。

 [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 Case1 { get { yield return new object[] { new MsgData { Code = "1" }, CustomEnum.Value1 }; } } public static IEnumerable Case2 { get { yield return new object[] { new MsgData { Code = "2" }, CustomEnum.Value2 }; } } public static IEnumerable Case3 { get { yield return new object[] { new MsgData { Code = "3" }, CustomEnum.Value3 }; } } public static IEnumerable Case4 { get { yield return new object[] { new MsgData { Code = "4" }, CustomEnum.Value4 }; } } 

但是,问题往往更通用(而不是具体),因为:

  1. xUnit.net通过非generics,无类型数组对参数化测试进行建模的方式
  2. 基于属性的模型确实使这些测试用例看起来像二等公民
  3. 所有这些类型声明和大括号的语言噪音

对于1.2.以及用于参数化测试的现有xUnit.net模型,没有什么可做的。


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

 let Case1 : seq = seq { yield [| { Code = "1" }; Value1 |] } let Case2 : seq = seq { yield [| { Code = "2" }; Value2 |] } let Case3 : seq = seq { yield [| { Code = "3" }; Value3 |] } let Case4 : seq = seq { yield [| { Code = "4" }; Value4 |] } [] [] [] [] [] let ShouldMapEnum (msgData, expected, sut : SomeObject) = let actual = sut.GetEnum(msgData) Assert.Equal(expected, actual.Value) 

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

 type MsgData = { Code : string } [] 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 [] type AutoMoqPropertyDataAttribute (dataProperty) = inherit CompositeDataAttribute( PropertyDataAttribute(dataProperty), AutoDataAttribute()) 

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

 [Theory] [PropertyAutoData("ColorPairs")] public void ReverseColors([TestCaseParameter] TestData testData, int autoGenValue) { ... } public static IEnumerable 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 testData上的[TestCaseParameter]属性。 这表示将从属性提供参数值。 它需要明确指定,因为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; ///  /// 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. ///  public class PropertyAutoDataAttribute : AutoDataAttribute { private readonly string _propertyName; public PropertyAutoDataAttribute(string propertyName) { _propertyName = propertyName; } public PropertyAutoDataAttribute(string propertyName, IFixture fixture) : base(fixture) { _propertyName = propertyName; } ///  /// Gets or sets the scope of auto-generated data. ///  public AutoDataScope Scope { get; set; } public override IEnumerable 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(); 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 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++]; } } } ///  /// Defines the scope of auto-generated data in a theory. ///  public enum AutoDataScope { ///  /// Data is auto-generated only once for all tests. ///  TestCase, ///  /// Data is auto-generated for every test. ///  Test } ///  /// Indicates that the parameter is part of a test case rather than being auto-generated by AutoFixture. ///  [AttributeUsage(AttributeTargets.Parameter)] public class TestCaseParameterAttribute : Attribute { }