Как запустить тестовый метод с несколькими параметрами в MSTest?

NUnit имеет функцию под названием Значения, как показано ниже:

[Test]
public void MyTest(
    [Values(1,2,3)] int x,
    [Values("A","B")] string s)
{
    // ...
}

Это означает, что метод теста будет запущен 6 раз:

MyTest(1, "A")
MyTest(1, "B")
MyTest(2, "A")
MyTest(2, "B")
MyTest(3, "A")
MyTest(3, "B")

Сейчас мы используем MSTest, есть ли какой-нибудь эквивалент для этого, чтобы я мог запустить один и тот же тест с несколькими параметрами?

[TestMethod]
public void Mytest()
{
    // ...
}

11 ответов

Решение

К сожалению, это не поддерживается в MSTest. Очевидно, что существует модель расширяемости, и вы можете реализовать ее самостоятельно. Другим вариантом будет использование управляемых данными тестов.

Мое личное мнение было бы просто придерживаться NUnit, хотя...

РЕДАКТИРОВАТЬ: Начиная с Visual Studio 2012, обновление 1, MSTest имеет аналогичную функцию. Смотрите ответ @McAden ниже.

РЕДАКТИРОВАТЬ 4: Похоже, что это завершено в MSTest V2 17 июня 2016 года: https://blogs.msdn.microsoft.com/visualstudioalm/2016/06/17/taking-the-mstest-framework-forward-with-mstest-v2/

Оригинальный ответ:

Примерно неделю назад в Visual Studio 2012 Update 1 теперь возможно нечто подобное:

[DataTestMethod]
[DataRow(12,3,4)]
[DataRow(12,2,6)]
[DataRow(12,4,3)]
public void DivideTest(int n, int d, int q)
{
  Assert.AreEqual( q, n / d );
}

РЕДАКТИРОВАТЬ: кажется, это доступно только в рамках проекта модульного тестирования для WinRT / Metro. лентяй

РЕДАКТИРОВАТЬ 2: Ниже приведены метаданные, найденные с помощью "Перейти к определению" в Visual Studio:

#region Assembly Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll, v11.0.0.0
// C:\Program Files (x86)\Microsoft SDKs\Windows\v8.0\ExtensionSDKs\MSTestFramework\11.0\References\CommonConfiguration\neutral\Microsoft.VisualStudio.TestPlatform.UnitTestFramework.dll
#endregion

using System;

namespace Microsoft.VisualStudio.TestPlatform.UnitTestFramework
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class DataTestMethodAttribute : TestMethodAttribute
    {
        public DataTestMethodAttribute();

        public override TestResult[] Execute(ITestMethod testMethod);
    }
}

РЕДАКТИРОВАТЬ 3: Эта проблема была поднята на форумах UserVoice Visual Studio. Последнее обновление гласит:

НАЧАЛО · Команда Visual Studio ADMIN Команда Visual Studio (команда разработчиков, Microsoft Visual Studio) ответила · 25 апреля 2016 г. Благодарим вас за отзыв. Мы начали работать над этим.

Пратап Лакшман Visual Studio

https://visualstudio.uservoice.com/forums/330519-team-services/suggestions/3865310-allow-use-of-datatestmethod-datarow-in-all-unit

Эта функция находится в предварительной версии и работает с VS 2015.

Например:

[TestClass]
public class UnitTest1
{
    [DataTestMethod]
    [DataRow(1, 2, 2)]
    [DataRow(2, 3, 5)]
    [DataRow(3, 5, 8)]
    public void AdditionTest(int a, int b, int result)
    {
        Assert.AreEqual(result, a + b);
    }
}

Так как никто не упомянул - не совсем то же самое, что NUnit Value (или же TestCase) атрибуты, но MSTest имеет DataSource атрибут, который позволяет вам делать подобные вещи. Вы можете подключить его к базе данных или XML-файлу - не так просто, как функция NUnit, но выполняет свою работу.

MSTest обладает мощным атрибутом DataSource, с помощью которого вы можете выполнять управляемый данными тест, как вы и просили. Вы можете иметь свои тестовые данные в XML, CSV или в базе данных. Вот несколько ссылок, которые помогут вам

http://visualstudiomagazine.com/articles/2009/09/15/unit-testing-with-vsts2008-part-3.aspx http://msdn.microsoft.com/en-us/library/ms182527.aspx
http://msdn.microsoft.com/en-us/library/ms243192.aspx

Надеюсь, что это поможет вам.

Это очень просто реализовать - вы должны использовать TestContext собственность и TestPropertyAttribute,

пример

public TestContext TestContext { get; set; }
private List<string> GetProperties()
{
    return TestContext.Properties
        .Cast<KeyValuePair<string, object>>()
        .Where(_ => _.Key.StartsWith("par"))
        .Select(_ => _.Value as string)
        .ToList();
}

//usage
[TestMethod]
[TestProperty("par1", "http://getbootstrap.com/components/")]
[TestProperty("par2", "http://www.wsj.com/europe")]
public void SomeTest()
{
    var pars = GetProperties();
    //...
}

Я не мог получить DataRowAttribute работать в Visual Studio 2015, это то, что я закончил с:

[TestClass]
public class Tests
{
    private Foo _toTest;

    [TestInitialize]
    public void Setup()
    {
        this._toTest = new Foo();       
    }

    [TestMethod]
    public void ATest()
    {
        this.Perform_ATest(1, 1, 2);
        this.Setup();

        this.Perform_ATest(100, 200, 300);
        this.Setup();

        this.Perform_ATest(817001, 212, 817213);
        this.Setup();

    }

    private void Perform_ATest(int a, int b, int expected)
    {
        //Obviously this would be way more complex...

        Assert.IsTrue(this._toTest.Add(a,b) == expected);    
    }
}

public class Foo
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

Реальное решение здесь - просто использовать NUnit (если вы не застряли в MSTest, как я в данном конкретном случае).

MsTest не поддерживает эту функцию, но вы можете реализовать свой собственный атрибут для достижения этого. посмотрите на ниже:

http://blog.drorhelper.com/2011/09/enabling-parameterized-tests-in-mstest.html

Есть, конечно, другой способ сделать это, который не обсуждался в этом потоке, то есть путем наследования класса, содержащего TestMethod. В следующем примере был определен только один TestMethod, но было выполнено два теста.

В Visual Studio 2012 он создает два теста в TestExplorer:

  1. DemoTest_B10_A5.test
  2. DemoTest_A12_B4.test

    public class Demo
    {
        int a, b;
    
        public Demo(int _a, int _b)
        {
            this.a = _a;
            this.b = _b;
        }
    
        public int Sum()
        {
            return this.a + this.b;
        }
    }
    
    public abstract class DemoTestBase
    {
        Demo objUnderTest;
        int expectedSum;
    
        public DemoTestBase(int _a, int _b, int _expectedSum)
        {
            objUnderTest = new Demo(_a, _b);
            this.expectedSum = _expectedSum;
        }
    
        [TestMethod]
        public void test()
        {
            Assert.AreEqual(this.expectedSum, this.objUnderTest.Sum());
        }
    }
    
    [TestClass]
    public class DemoTest_A12_B4 : DemoTestBase
    {
        public DemoTest_A12_B4() : base(12, 4, 16) { }
    }
    
    public abstract class DemoTest_B10_Base : DemoTestBase
    {
        public DemoTest_B10_Base(int _a) : base(_a, 10, _a + 10) { }
    }
    
    [TestClass]
    public class DemoTest_B10_A5 : DemoTest_B10_Base
    {
        public DemoTest_B10_A5() : base(5) { }
    }
    

В примере OP речь шла о функции NUnit, которая легко позволяет получить декартово произведение предоставленных значений. Насколько я мог судить, ни один ответ здесь не охватывал эту часть. Я воспринял это как небольшую проблему и в итоге получил следующую реализацию.

[Изменить: рефакторинг на основе массива + значения Zip]

Я провел некоторый рефакторинг исходной версии на основе Enumerator (см. историю сообщений), чтобы теперь вместо этого использовать только массивы и индексы циклических проходов. Я также воспользовался возможностью и добавил новый тип значений Zip, который будет соответствовать разным значениям для каждого набора, созданного декартовым продуктом. Это может быть полезно, например, для добавления ожидаемого результата.

Он все еще не оптимизирован, поэтому не стесняйтесь предлагать улучшения.

      #nullable enable
public enum ValuesType
{
    Undefined = 0,
    Cartesian = 1,
    /// <summary>
    /// Values will be <see cref="Enumerable.Zip{TFirst, TSecond, TResult}(IEnumerable{TFirst}, IEnumerable{TSecond}, Func{TFirst, TSecond, TResult})">Zipped</see> with the cartesian produce of the other parameters.
    /// </summary>
    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}.");
        }

        //Since we are using ValueTuples, it is essential that once we created our collection, we stick to it. If we were to create a new one, we would end up with a copy of the ValueTuples that won't be synced anymore.
        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)
{
    //Arrange / Act / Assert
    //Cases would be
    // a1, 1, false, System.ConsoleModifiers.Alt, 1
    // a1, 1, false, System.ConsoleModifiers.Shift, 2
    // a1, 1, false, System.ConsoleModifiers.Control, 3
    // a1, 1, true, System.ConsoleModifiers.Alt, 4
    // a1, 1, true, System.ConsoleModifiers.Shift, 5
    // a1, 1, true, System.ConsoleModifiers.Control, 6
    // a1, 2, false, System.ConsoleModifiers.Alt, 7
    // a1, 2, false, System.ConsoleModifiers.Shift, 8
    // a1, 2, false, System.ConsoleModifiers.Control, 9
    // a1, 2, true, System.ConsoleModifiers.Alt, 10
    // a1, 2, true, System.ConsoleModifiers.Shift, 11
    // a1, 2, true, System.ConsoleModifiers.Control, 12
    // a2, 1, false, System.ConsoleModifiers.Alt, 13
    // a2, 1, false, System.ConsoleModifiers.Shift, 14
    // a2, 1, false, System.ConsoleModifiers.Control, 15
    // a2, 1, true, System.ConsoleModifiers.Alt, 16
    // a2, 1, true, System.ConsoleModifiers.Shift, 17
    // a2, 1, true, System.ConsoleModifiers.Control, 18
    // a2, 2, false, System.ConsoleModifiers.Alt, 19
    // a2, 2, false, System.ConsoleModifiers.Shift, 20
    // a2, 2, false, System.ConsoleModifiers.Control, 21
    // a2, 2, true, System.ConsoleModifiers.Alt, 22
    // a2, 2, true, System.ConsoleModifiers.Shift, 23
    // a2, 2, true, System.ConsoleModifiers.Control, 24
}

Вот переосмыслениеNUnits,[Sequential]и .

В отличие отNUnit, что предполагает[Combinatorial]по умолчанию, вMSTestмы всегда должны указывать, какой из них мы хотим, иначе[Values]атрибут не будет иметь никакого эффекта.

Использование:

      [TestClass]
public class TestMethods
{
    [TestMethod, Combinatorial]
    public void EnumIterationTestMethod(Season season) => Console.WriteLine(season);

    [TestMethod, Combinatorial]
    public void BoolIterationTestMethod(bool boolean) => Console.WriteLine(boolean);

    [TestMethod, Combinatorial]
    public void CombinatoralValuesIterationTestMethod(Season season, bool boolean) => Console.WriteLine($"{season} {boolean}");

    [TestMethod, Sequential]
    public void SequentialCombinatoralIterationTestMethod(
    [Values(1, 2, 3)] int param1,
    [Values("A", "B")] string param2) => Console.WriteLine($"{param1} {param2 ?? "null"}");

    [TestMethod, Combinatorial]
    public void CombinatoralIterationTestMethod(
    [Values(1, 2, 3)] int param1,
    [Values("A", "B")] string param2) => Console.WriteLine($"{param1} {param2 ?? "null"}");
}

Код:

      [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
public class ValuesAttribute : Attribute
{
    public object?[] Values { get; }

    public ValuesAttribute(params object?[] values)
    {
        Values = values;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CombinatorialAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        var values = Utils.GetPossibleValuesForEachParameter(methodInfo);
        return Utils.CreateCombinations(values);
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        if (data != null)
        {
            return $"{methodInfo.Name} ({string.Join(", ", data.Select(e => e ?? "null"))})";
        }

        return null;
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class SequentialAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        var values = Utils.GetPossibleValuesForEachParameter(methodInfo);
        return Utils.ZipLongestFillWithNull(values);
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        if (data != null)
        {
            return $"{methodInfo.Name} ({string.Join(", ", data.Select(e => e ?? "null"))})";
        }

        return null;
    }
}

public static class Utils
{
    public static List<List<object?>> GetPossibleValuesForEachParameter(MethodInfo methodInfo)
    {
        List<List<object?>> values = new();

        foreach (var parameter in methodInfo.GetParameters())
        {
            var attribute = parameter.GetCustomAttribute<ValuesAttribute>();

            if (attribute == null || attribute.Values.Length == 0)
            {
                if (parameter.ParameterType.IsEnum)
                {
                    values.Add(Enum.GetValues(parameter.ParameterType).Cast<object?>().ToList());
                    continue;
                }

                if (parameter.ParameterType == typeof(bool))
                {
                    values.Add(new List<object?> { true, false });
                    continue;
                }

                if (attribute == null)
                {
                    throw new InvalidOperationException($"{parameter.Name} should have a [Values(...)] attribute set");
                }
                else
                {
                    throw new InvalidOperationException($"[Values] {parameter.ParameterType} {parameter.Name} is only valid for Enum or Boolean types. Consider using the attribute constructor [Values(...)].");
                }
            }

            values.Add(attribute.Values.ToList());
        }

        return values;
    }

    public static IEnumerable<object?[]> ZipLongestFillWithNull(List<List<object?>> values)
    {
        var longest = values.Max(e => e.Count);

        foreach (var list in values)
        {
            if (list.Count < longest)
            {
                var diff = longest - list.Count;
                list.AddRange(Enumerable.Repeat<object?>(null, diff));
            }
        }

        for (int i = 0; i < longest; i++)
        {
            yield return values.Select(e => e[i]).ToArray();
        }
    }

    public static IEnumerable<object?[]> CreateCombinations(List<List<object?>> values)
    {
        var indices = new int[values.Count];

        while (true)
        {
            // Create new arguments
            var arg = new object?[indices.Length];
            for (int i = 0; i < indices.Length; i++)
            {
                arg[i] = values[i][indices[i]];
            }

            yield return arg!;

            // Increment indices
            for (int i = indices.Length - 1; i >= 0; i--)
            {
                indices[i]++;
                if (indices[i] >= values[i].Count)
                {
                    indices[i] = 0;

                    if (i == 0)
                        yield break;
                }
                else
                    break;
            }
        }
    }
}
Другие вопросы по тегам