问题
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:
- the way xUnit.net models parameterized tests via non-generic, untyped, arrays
- the attribute-based model which really makes these test cases look like second-class citizens
- 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