Автофиксирование, смешивание PropertyData с несколькими записями и AutoData (используя AutoMoqCustomization)

Я посмотрел на оба из этих похожих вопросов:

И они потрясающие, и меня почти нет. Но оба примера используют только одну запись в передаваемом 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 }; } }

Однако проблема имеет тенденцию быть более общей (а не специфической) из-за:

  1. способ, которым xUnit.net моделирует параметризованные тесты через неуниверсальные, нетипизированные массивы
  2. модель на основе атрибутов, которая действительно делает эти тестовые примеры похожими на граждан второго сорта
  3. шум от языка со всеми этими объявлениями типов и фигурными скобками

За 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
{
}
Другие вопросы по тегам