Передайте сложные параметры в [Теорию]

Xunit имеет приятную особенность: вы можете создать один тест с Theory приписать и поместить данные в InlineData атрибуты, и xUnit сгенерирует много тестов и проверит их все.

Я хочу иметь что-то вроде этого, но параметры моего метода не являются "простыми данными" (например, string, int, double), но список моего класса:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }

13 ответов

Решение

Здесь очень много xxxxData атрибуты в XUnit. Проверьте, например, PropertyData приписывать.

Вы можете реализовать свойство, которое возвращает IEnumerable<object[]>, каждый object[] что этот метод генерирует будет затем "распакован" в качестве параметров для одного вызова к вашему [Theory] метод.

Другой вариант ClassData, который работает так же, но позволяет легко разделять "генераторы" между тестами в разных классах / пространствах имен, а также отделяет "генераторы данных" от реальных методов тестирования.

Смотрите, например, эти примеры здесь:

Пример PropertyData

public class StringTests2
{
    [Theory, PropertyData(nameof(SplitCountData))]
    public void SplitCount(string input, int expectedCount)
    {
        var actualCount = input.Split(' ').Count();
        Assert.Equal(expectedCount, actualCount);
    }

    public static IEnumerable<object[]> SplitCountData
    {
        get
        {
            // Or this could read from a file. :)
            return new[]
            {
                new object[] { "xUnit", 1 },
                new object[] { "is fun", 2 },
                new object[] { "to test with", 3 }
            };
        }
    }
}

Пример ClassData

public class StringTests3
{
    [Theory, ClassData(typeof(IndexOfData))]
    public void IndexOf(string input, char letter, int expected)
    {
        var actual = input.IndexOf(letter);
        Assert.Equal(expected, actual);
    }
}

public class IndexOfData : IEnumerable<object[]>
{
    private readonly List<object[]> _data = new List<object[]>
    {
        new object[] { "hello world", 'w', 6 },
        new object[] { "goodnight moon", 'w', -1 }
    };

    public IEnumerator<object[]> GetEnumerator()
    { return _data.GetEnumerator(); }

    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}

Предположим, что у нас есть сложный класс Car с классом производителя:

public class Car
{
     public int Id { get; set; }
     public long Price { get; set; }
     public Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
    public string Name { get; set; }
    public string Country { get; set; }
}

Мы собираемся заполнить и сдать класс автомобиля на тест теории.

Поэтому создайте класс CarClassData, который возвращает экземпляр класса Car, как показано ниже:

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="country",
                    Name="name"
                  }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

Пришло время создать тестовый метод (CarTest) и определить автомобиль в качестве параметра:

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(Car car)
{
     var output = car;
     var result = _myRepository.BuyCar(car);
}

Удачи

Чтобы обновить ответ @ Кецалькоатля: Атрибут [PropertyData] был заменен [MemberData] который принимает в качестве аргумента строковое имя любого статического метода, поля или свойства, которое возвращает IEnumerable<object[]>, (Мне особенно приятно иметь метод итератора, который на самом деле может вычислять контрольные примеры по одному, получая их по мере их вычисления.)

Каждый элемент в последовательности, возвращаемой перечислителем, является object[] и каждый массив должен иметь одинаковую длину, и эта длина должна быть числом аргументов вашего теста (помечено атрибутом [MemberData] и каждый элемент должен иметь тот же тип, что и соответствующий параметр метода. (Или, может быть, они могут быть конвертируемыми, я не знаю.)

(См. Примечания к выпуску для xUnit.net, март 2014 г. и актуальный патч с примером кода.)

Создание массивов анонимных объектов - не самый простой способ построения данных, поэтому я использовал этот шаблон в своем проекте.

Сначала определите некоторые повторно используемые общие классы

//http://stackru.com/questions/22093843
public interface ITheoryDatum
{
    object[] ToParameterArray();
}

public abstract class TheoryDatum : ITheoryDatum
{
    public abstract object[] ToParameterArray();

    public static ITheoryDatum Factory<TSystemUnderTest, TExecptedOutput>(TSystemUnderTest sut, TExecptedOutput expectedOutput, string description)
    {
        var datum= new TheoryDatum<TSystemUnderTest, TExecptedOutput>();
        datum.SystemUnderTest = sut;
        datum.Description = description;
        datum.ExpectedOutput = expectedOutput;
        return datum;
    }
}

public class TheoryDatum<TSystemUnderTest, TExecptedOutput> : TheoryDatum
{
    public TSystemUnderTest SystemUnderTest { get; set; }

    public string Description { get; set; }

    public TExecptedOutput ExpectedOutput { get; set; }

    public override object[] ToParameterArray()
    {
        var output = new object[3];
        output[0] = SystemUnderTest;
        output[1] = ExpectedOutput;
        output[2] = Description;
        return output;
    }

}

Теперь ваши индивидуальные тесты и данные о членах легче записывать и чище...

public class IngredientTests : TestBase
{
    [Theory]
    [MemberData(nameof(IsValidData))]
    public void IsValid(Ingredient ingredient, string testDescription, bool expectedResult)
    {
        Assert.True(ingredient.IsValid == expectedResult, testDescription);
    }

    public static IEnumerable<object[]> IsValidData
    {
        get
        {
            var food = new Food();
            var quantity = new Quantity();
            var data= new List<ITheoryDatum>();

            data.Add(TheoryDatum.Factory(new Ingredient { Food = food }                       , false, "Quantity missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity }               , false, "Food missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity, Food = food }  , true,  "Valid"));

            return data.ConvertAll(d => d.ToParameterArray());
        }
    }
}

Строка Description свойство бросить себе кость, когда один из ваших многочисленных тестов неудачен

Для своих нужд я просто хотел провести серию "тестовых пользователей" через несколько тестов - но [ClassData] и т. Д. Казались излишними для того, что мне было нужно (потому что список элементов был локализован для каждого теста).

Итак, я сделал следующее, с массивом внутри теста - проиндексированным снаружи:

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public async Task Account_ExistingUser_CorrectPassword(int userIndex)
{
    // DIFFERENT INPUT DATA (static fake users on class)
    var user = new[]
    {
        EXISTING_USER_NO_MAPPING,
        EXISTING_USER_MAPPING_TO_DIFFERENT_EXISTING_USER,
        EXISTING_USER_MAPPING_TO_SAME_USER,
        NEW_USER

    } [userIndex];

    var response = await Analyze(new CreateOrLoginMsgIn
    {
        Username = user.Username,
        Password = user.Password
    });

    // expected result (using ExpectedObjects)
    new CreateOrLoginResult
    {
        AccessGrantedTo = user.Username

    }.ToExpectedObject().ShouldEqual(response);
}

Это достигло моей цели, сохраняя при этом цель теста ясной. Вам просто нужно синхронизировать индексы, но это все.

Хорошо выглядит в результатах, он сворачивается, и вы можете перезапустить конкретный экземпляр, если получите ошибку:

Вы можете попробовать так:

public class TestClass {

    bool isSaturday(DateTime dt)
    {
       string day = dt.DayOfWeek.ToString();
       return (day == "Saturday");
    }

    [Theory]
    [MemberData("IsSaturdayIndex", MemberType = typeof(TestCase))]
    public void test(int i)
    {
       // parse test case
       var input = TestCase.IsSaturdayTestCase[i];
       DateTime dt = (DateTime)input[0];
       bool expected = (bool)input[1];

       // test
       bool result = isSaturday(dt);
       result.Should().Be(expected);
    }   
}

Создайте другой класс для хранения тестовых данных:

public class TestCase
{
   public static readonly List<object[]> IsSaturdayTestCase = new List<object[]>
   {
      new object[]{new DateTime(2016,1,23),true},
      new object[]{new DateTime(2016,1,24),false}
   };

   public static IEnumerable<object[]> IsSaturdayIndex
   {
      get
      {
         List<object[]> tmp = new List<object[]>();
            for (int i = 0; i < IsSaturdayTestCase.Count; i++)
                tmp.Add(new object[] { i });
         return tmp;
      }
   }
}

Вот как я решил вашу проблему, у меня был такой же сценарий. Таким образом, встроены настраиваемые объекты и разное количество объектов при каждом запуске.

    [Theory]
    [ClassData(typeof(DeviceTelemetryTestData))]
    public async Task ProcessDeviceTelemetries_TypicalDeserialization_NoErrorAsync(params DeviceTelemetry[] expected)
    {
        // Arrange
        var timeStamp = DateTimeOffset.UtcNow;

        mockInflux.Setup(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>())).ReturnsAsync("Success");

        // Act
        var actual = await MessageProcessingTelemetry.ProcessTelemetry(JsonConvert.SerializeObject(expected), mockInflux.Object);

        // Assert
        mockInflux.Verify(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>()), Times.Once);
        Assert.Equal("Success", actual);
    }

Это мой модульный тест, обратите внимание на параметр params. Это позволяет отправить другое количество объектов. А теперь мой класс DeviceTelemetryTestData:

    public class DeviceTelemetryTestData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

Надеюсь, это поможет!

Вот мое решение проблемы.

https://github.com/xunit/xunit/issues/2760

Преимущество в том, что оно скрываетyields и перечислители из пользовательского кода.

Введите новый атрибут и интерфейс.

      public class InlineObjectDataAttribute<T> : ClassDataAttribute where T : IInlineObjects
{
    public InlineObjectDataAttribute() : base(typeof(GenericTestData)) { }

    class GenericTestData : IEnumerable<object?[]>
    {
        public IEnumerator<object?[]> GetEnumerator()
        {
            foreach (var item in T.GetObjects())
            {
                yield return item;
            }
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

public interface IInlineObjects
{
    static abstract IEnumerable<object?[]> GetObjects();
    static object?[] Line(params object?[] data) => data;
}

В вашем тестовом классе используйте следующее:

      using static IInlineObjects;

class MyTestClass : IInlineObjects
{
    public static IEnumerable<object?[]> GetObjects() => new object?[][]
    {
        Line(180d, new DateTime(2000, 1, 1, 6, 0, 0)),
    };
}

[Theory]
[InlineObjectData<MyTestClass>]
public void TestMethod(object o1, object o2)

Несмотря на то, что на это уже был дан ответ, я просто хочу добавить здесь улучшение.

Ограничение передачи объектов в атрибуте InlineData — это не ограничение самого xUnit, а атрибуты C#.

См. эту ошибку компилятора: Ошибка компилятора CS0182

Вы можете использовать TheoryDataдля сложных типов, таких как классы.

      [Theory, MemberData(nameof(CustomClassTests))]
public async Task myTestName(MyCustomClass customClassTestData) { ... }

public record MyCustomClass { ... }

public static TheoryData<MyCustomClass> CustomClassTests {
    get {
        return new() {
            new MyCustomClass{ ... },
            new MyCustomClass{ ... },
            ...
        }; 
    }
}

Вот мой способ определения сложных параметров как TheoryData.

  1. Определить общую структуру параметров
      public struct ExpectedValueTestData<TParameters, TExpected>
{
    public string Name;
    public TParameters Params;
    public TExpected ExpectedValue;

    public override string ToString()
    {
        return $"{this.Name}";
    }
}
  1. Подготовьте сложные данные, используя данные теории.
      public class ValidValueTests : TheoryData<ExpectedValueTestData<Parameters, BoolMessage>>
        {
            private readonly Fixture _fixture;
            public ValidValueTests()
            {
                _fixture = new Fixture();
                this.Add(new ExpectedValueTestData<Parameters, BoolMessage>
                {
                    Name = @"Event Name - valid call for create",
                    Params = new Parameters
                    {
                        request = _fixture.Build<EventData>()
                                                    .With(data => data.Id, 1001)
                                                    .With(data => data.Operation, Operation.Create)
                                                    .Create(),
                        context = null
                    },
                    ExpectedValue = new BoolMessage { Status = true },
                });
                this.Add(new ExpectedValueTestData<Parameters, BoolMessage>
                {
                    Name = @"Event Name - valid call for update",
                    Params = new Parameters
                    {
                        request = _fixture.Build<EventData>()
                                                    .With(data => data.Id, 1001)
                                                    .With(data => data.Operation, Operation.Update)
                                                    .OmitAutoProperties()
                                                    .Create(),
                        context = null
                    },
                    ExpectedValue = new BoolMessage { Status = true },
                });
            }
        }

для получения более подробной информации перейдите по этой ссылке - https://medium.com/@sanjaysoni_48818/xunit2-automoq-autofixture-trilogy-44ee8598f281 .

Удачи

xUnit.Sdkпредоставляет вам DataAttributeкласс, который вы могли бы наследовать и переопределить GetDataметод и используйте его, чтобы передать все, что вы хотите.

Я обычно использую его вместе с шаблоном DataTestBuilders и создаю что-то подобное.

      public class ValidComplexObjectDataSource : DataAttribute
{
    public override IEnumerable<object[]> GetData(MethodInfo testMethod)
    {
        yield return new object[] {
            ComplexObjectBuilder
                .BasicComplexObject()
                .Build()
        };

        yield return new object[] {
            ComplexObjectBuilder
                .BasicComplexObject()
                .WithoutSomeAttribute()
                .Build()
        };

       // ... list all test cases you want to pass to your method
    }
}

Этот ComplexObjectBuilderможет быть любым вашим объектом, настоятельно рекомендую проверить шаблон построителя

      [Theory]
[Trait("Validation", "CreateXYZCommand")]
[ValidComplexObjectDataSource]
public void CreateXYZCommandValidator_WithValidInput_ShouldPassAllValidations(CreateComplexObjectInput createComplexObjectInput)
{
    var command = new CreateXYZCommand(createComplexObjectInput);
    var result = _validator.TestValidate(command);
    result.ShouldNotHaveAnyValidationErrors();
}

Я продемонстрировал это только с одним объектом, у вас есть массив объектов, которые вы можете получить.

      yield return new object[] {
   ComplexObject_1,
   ComplexObject_2,
   string_attribute,
   int_attribute
};

и используйте их в качестве аргументов для ваших тестовых случаев.

Я думаю, вы ошиблись здесь. Что такое xUnit Theory Атрибут фактически означает: вы хотите протестировать эту функцию, отправив специальные / случайные значения в качестве параметров, которые получает эта тестируемая функция. Это означает, что то, что вы определяете как следующий атрибут, например: InlineData, PropertyData, ClassDataи т. д. будет источником этих параметров. Это означает, что вы должны создать исходный объект для предоставления этих параметров. В вашем случае, я думаю, вы должны использовать ClassData объект как источник. Также обратите внимание, что ClassData наследуется от: IEnumerable<> - это означает, что каждый раз другой набор сгенерированных параметров будет использоваться в качестве входящих параметров для тестируемой функции до IEnumerable<> производит значения.

Пример здесь: Том Дюпон.NET

Пример может быть неверным - я не использовал xUnit в течение длительного времени

Другие вопросы по тегам