OP的例子是关于NUnit的一个特性,它可以轻松地生成提供的值的笛卡尔积。据我所知,这里没有人回答到这一部分。我把它看作是一个小挑战,并最终得到了以下的实现。
[编辑:基于数组的重构+Zip值]
我对原始的基于Enumerator的版本进行了一些重构(请参阅帖子历史),现在只使用数组和循环遍历索引。我还利用这个机会添加了一种新的Zip值类型,它将为笛卡尔积创建的每个集合匹配一个不同的值。这可能对于添加ExpectedResult非常有用。
它仍然不是真正优化过的,所以请随时提出改进建议。
#nullable enable
public enum ValuesType
{
Undefined = 0,
Cartesian = 1,
Zip = 2
}
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
public ValuesType ValuesType { get; }
public object[] Values { get; }
public ValuesAttribute(params object[] values)
: this(ValuesType.Cartesian, values)
{ }
public ValuesAttribute(ValuesType valuesType, params object[] values)
{
ValuesType = valuesType;
Values = values;
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ValuesDataSourceAttribute : Attribute, ITestDataSource
{
public IEnumerable<object[]> GetData(MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();
var values = new (ValuesType Type, object[] Values, int Index)[parameters.Length];
for(var i=0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var attribute = parameter.GetCustomAttribute<ValuesAttribute>();
if (attribute != null)
{
if (attribute.Values.Any(v => !parameter.ParameterType.IsAssignableFrom(v.GetType())))
throw new InvalidOperationException($"All values of {nameof(ValuesAttribute)} must be of type {parameter.ParameterType.Name}. ParameterName: {parameter.Name}.");
switch (attribute.ValuesType)
{
case ValuesType.Cartesian:
values[i] = (ValuesType.Cartesian, attribute.Values, 0);
break;
case ValuesType.Zip:
values[i] = (ValuesType.Zip, attribute.Values, 0);
break;
}
}
else if (parameter.ParameterType == typeof(bool))
values[i] = (ValuesType.Cartesian, new object[] { false, true }, 0);
else if (parameter.ParameterType.IsEnum)
values[i] = (ValuesType.Cartesian, Enum.GetValues(parameter.ParameterType).Cast<Object>().ToArray(), 0);
else
throw new InvalidOperationException($"All parameters must have either {nameof(ValuesAttribute)} attached or be a bool or an Enum . ParameterName: {parameter.Name}.");
}
var cartesianTotalCount = values.Where(v => v.Type == ValuesType.Cartesian).Aggregate(1, (actualTotal, currentValues) => actualTotal * currentValues.Values.Length);
if (values.Any(v => v.Type == ValuesType.Zip && v.Values.Length != cartesianTotalCount))
throw new InvalidOperationException($"{nameof(ValuesType.Zip)} typed attributes must have as many values as the produce of all the others ({cartesianTotalCount}).");
bool doIncrement;
for(var globalPosition = 0; globalPosition < cartesianTotalCount; globalPosition++)
{
yield return values.Select(v => v.Values[v.Index]).ToArray();
doIncrement = true;
for (var i = values.Length - 1; i >= 0 && doIncrement; i--)
{
switch (values[i].Type)
{
case ValuesType.Zip:
values[i].Index++;
break;
case ValuesType.Cartesian:
if (doIncrement && ++values[i].Index >= values[i].Values.Length)
values[i].Index = 0;
else
doIncrement = false;
break;
default:
throw new InvalidOperationException($"{values[i].Type} is not supported.");
}
}
}
}
public string GetDisplayName(MethodInfo methodInfo, object[] data)
{
return data.JoinStrings(" / ");
}
}
使用方法:
[TestMethod]
[ValuesDataSource]
public void Test([Values("a1", "a2")] string a, [Values(1, 2)] int b, bool c, System.ConsoleModifiers d, [Values(ValuesType.Zip, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)] int asserts)
{
}