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

100

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

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

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }
зчпить
источник
1
Полное руководство, которое отправляет сложные объекты в качестве параметра для сложных типов
модульном

Ответы:

139

В xxxxDataXUnit есть много атрибутов. Посмотрите, например, 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(); }
}
Кецалькоатль
источник
@dcastro: да, я на самом деле искал некоторые на оригинальные XUnit документы
Кетцалькоатль
2
@Nick: Я согласен , что это похоже на PropertyData, но и вы указали причину этого: static. Именно поэтому я бы не стал. ClassData - это когда вы хотите уйти от статики. Поступая таким образом, вы можете легче повторно использовать (то есть вкладывать) генераторы.
quetzalcoatl
1
Есть идеи, что случилось с ClassData? Я не могу найти его в xUnit2.0, на данный момент я использую MemberData со статическим методом, который создает новый экземпляр класса и возвращает его.
Эрти-Крис Элмаа
14
@Erti, используйте [MemberData("{static member}", MemberType = typeof(MyClass))]для замены ClassDataатрибута.
Цзюнь Ли
7
Начиная с C # 6, рекомендуется использовать nameofключевое слово вместо жесткого кодирования имени свойства (ломается легко, но незаметно).
Сара
41

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

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

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

Давидбак
источник
3
@davidbak Кодплекса больше нет. Ссылка не работает
Кишан Вайшнав
11

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

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; }
}

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

Итак, создайте класс 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);
}

сложный тип в теории

Удачи

Иман Бахрампур
источник
4
В этом ответе явно рассматривается вопрос о передаче настраиваемого типа в качестве входных данных Theory, который, похоже, отсутствует в выбранном ответе.
JD Cain
1
Это именно тот вариант использования, который я искал: как передать сложный тип в качестве параметра теории. Прекрасно работает! Это действительно окупается для тестирования шаблонов MVP. Теперь я могу настроить множество различных экземпляров представления во всевозможных состояниях и передать их все в одну теорию, которая проверяет эффекты, которые методы Presenter оказывают на это представление. Любить это!
Денис М. Китчен
10

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

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

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

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

    public static ITheoryDatum Factory<TSystemUnderTest, TExpectedOutput>(TSystemUnderTest sut, TExpectedOutput expectedOutput, string description)
    {
        var datum= new TheoryDatum<TSystemUnderTest, TExpectedOutput>();
        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 TExpectedOutput 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, bool expectedResult, string testDescription)
    {
        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состоит в том, чтобы бросить себе кость, когда один из ваших многочисленных тестовых примеров не удался.

фиат
источник
1
Мне это нравится; у него есть реальный потенциал для очень сложного объекта, который мне нужно проверить на более чем 90 свойствах. Я могу передать простой объект JSON, десериализовать его и сгенерировать данные для тестовой итерации. Отличная работа.
Gustyn
1
не перепутались ли параметры IsValid Testmethod - разве это не должно быть IsValid (ингредиент, exprectedResult, testDescription)?
pastacool
3

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

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;
      }
   }
}
Sandy_Vu
источник
1

Для моих нужд я просто хотел запустить серию «тестовых пользователей» через несколько тестов - но [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);
}

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

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

введите описание изображения здесь

Simon_Weaver
источник
«Хорошо смотрится в результатах, он сворачивается, и вы можете повторно запустить конкретный экземпляр, если получите ошибку». Очень хороший момент. Главный недостаток, по- MemberDataвидимому, заключается в том, что вы не можете видеть или запускать тест с определенным тестовым входом. Это отстой.
Оливер Пирмейн,
На самом деле, я только что понял, что это возможно, MemberDataесли вы используете TheoryDataи по желанию IXunitSerializable. Дополнительная информация и примеры здесь ... github.com/xunit/xunit/issues/429#issuecomment-108187109
Оливер Пирмейн,
1

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

    [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();
    }

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

Max_Thom
источник
-1

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

Пример здесь: Tom DuPont .NET

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

Джаспер
источник