Автофиксирование, смешивание PropertyData с несколькими записями и AutoData (используя AutoMoqCustomization)
Я посмотрел на оба из этих похожих вопросов:
- Автофиксирование: PropertyData и гетерогенные параметры
- Автофиксирование CompositeDataAttribute не работает с PropertyDataAttribute
И они потрясающие, и меня почти нет. Но оба примера используют только одну запись в передаваемом IEnumerable PropertyData (то есть: yield return new object[] { 2, 4 };
- см.: /questions/32356558/avtofiksirovanie-propertydata-i-geterogennyie-parametryi/32356566#32356566) Это работает, но взрывается всякий раз, когда я хочу выполнить тестирование более чем одного объекта [] тестовых данных. У меня есть целая коллекция тестовых данных, которые я хочу отправить.
Я думаю, что ответ здесь ( /questions/26229202/avtofiksirovanie-compositedataattribute-ne-rabotaet-s-propertydataattribute/26229210#26229210) похож на то, что мне нужно, но я не могу понять это. Мне в основном нужно автофикс для создания sut
экземпляр для каждой итерации PropertyData.
Некоторая ссылка:
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()))
})
{ }
}
Что мне не хватает? Могу ли я смешивать атрибуты AutoFixture на основе PropertyData- и AutoData, как это, когда требуется несколько итераций данных PropertyData?
РЕДАКТИРОВАТЬ Вот трассировка стека исключений:
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 ответа
Вы должны предоставить контрольные примеры, как описано в этом ответе, на который указывает Рубен Бартелинк.
[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 }; } }
Однако проблема имеет тенденцию быть более общей (а не специфической) из-за:
- способ, которым xUnit.net моделирует параметризованные тесты через неуниверсальные, нетипизированные массивы
- модель на основе атрибутов, которая действительно делает эти тестовые примеры похожими на граждан второго сорта
- шум от языка со всеми этими объявлениями типов и фигурными скобками
За 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())
Я сам нуждался в этом, и я написал новый класс PropertyAutoData
который сочетает в себе PropertyData
и AutoFixture аналогично тому, как InlineAutoData
комбинаты InlineData
и автокрепление. Использование:
[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 } };
}
}
Обратите внимание [TestCaseParameter]
атрибут на парам testData
, Это указывает на то, что значение параметра будет предоставлено из свойства. Это должно быть явно указано, потому что тип AutoFixture изменил значение параметризованных тестов.
Выполнение этого дает 2 теста, как и ожидалось, в которых autoGenValue
имеет то же автоматически сгенерированное значение. Вы можете изменить это поведение, установив Scope
автоматически сгенерированных данных:
[PropertyAutoData("ColorPairs", Scope = AutoDataScope.Test)] // default is TestCase
Вы также можете использовать это вместе с SubSpec's 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
{
}