Передайте сложные параметры в [Теорию]
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
Преимущество в том, что оно скрываетyield
s и перечислители из пользовательского кода.
Введите новый атрибут и интерфейс.
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.
- Определить общую структуру параметров
public struct ExpectedValueTestData<TParameters, TExpected>
{
public string Name;
public TParameters Params;
public TExpected ExpectedValue;
public override string ToString()
{
return $"{this.Name}";
}
}
- Подготовьте сложные данные, используя данные теории.
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 в течение длительного времени