Поделиться через


Тестирование на основе данных в MSTest

Тестирование на основе данных позволяет выполнять один и тот же метод теста с несколькими наборами входных данных. Вместо написания отдельных методов тестирования для каждого тестового случая определите логику теста один раз и укажите различные входные данные с помощью атрибутов или внешних источников данных.

Обзор

MSTest предоставляет несколько атрибутов для тестирования на основе данных:

Свойство Сценарий использования Лучше всего подходит для
DataRow Встроенные тестовые данные Простые, статические тестовые случаи
DynamicData Данные из методов, свойств или полей Сложные или вычисляемые тестовые данные
DataSource Внешние файлы данных или базы данных Устаревшие сценарии с внешними источниками данных

MSTest также предоставляет следующие типы для расширения сценариев, управляемых данными:

  • TestDataRow<T>: тип возвращаемого значения для реализаций ITestDataSource (включая DynamicData), который добавляет поддержку метаданных, таких как отображаемые имена, категории и игнорируемые сообщения для отдельных тестовых случаев.
  • ITestDataSource: интерфейс, который можно реализовать в пользовательском атрибуте для создания полностью настраиваемых атрибутов источника данных.

Подсказка

Для комбинаторного тестирования (тестирование всех сочетаний нескольких наборов параметров) используйте пакет NuGet с открытым исходным кодом.MSTest . Этот пакет, поддерживаемый сообществом, доступен на сайте GitHub , но не поддерживается корпорацией Майкрософт.

DataRowAttribute

Данный DataRowAttribute позволяет запускать один и тот же метод тестирования с несколькими различными входными данными. Примените один или несколько DataRow атрибутов к методу теста и объедините его с TestMethodAttributeним.

Число и типы аргументов должны точно соответствовать сигнатуре метода теста.

Подсказка

Связанные анализаторы:

  • MSTEST0014 проверяет, соответствуют ли DataRow аргументы сигнатуре метода теста.
  • MSTEST0042 обнаруживает повторяющиеся DataRow записи, которые будут выполнять один и тот же тестовый случай несколько раз.

Базовое использование

[TestClass]
public class CalculatorTests
{
    [TestMethod]
    [DataRow(1, 2, 3)]
    [DataRow(0, 0, 0)]
    [DataRow(-1, 1, 0)]
    [DataRow(100, 200, 300)]
    public void Add_ReturnsCorrectSum(int a, int b, int expected)
    {
        var calculator = new Calculator();
        Assert.AreEqual(expected, calculator.Add(a, b));
    }
}

Поддерживаемые типы аргументов

DataRow поддерживает различные типы аргументов, включая примитивы, строки, массивы и значения NULL:

[TestClass]
public class DataRowExamples
{
    [TestMethod]
    [DataRow(1, "message", true, 2.0)]
    public void TestWithMixedTypes(int i, string s, bool b, float f)
    {
        // Test with different primitive types
    }

    [TestMethod]
    [DataRow(new string[] { "line1", "line2" })]
    public void TestWithArray(string[] lines)
    {
        Assert.AreEqual(2, lines.Length);
    }

    [TestMethod]
    [DataRow(null)]
    public void TestWithNull(object o)
    {
        Assert.IsNull(o);
    }

    [TestMethod]
    [DataRow(new string[] { "a", "b" }, new string[] { "c", "d" })]
    public void TestWithMultipleArrays(string[] input, string[] expected)
    {
        // Starting with MSTest v3, two arrays don't need wrapping
    }
}

Использование парам для аргументов переменной

Используйте ключевое params слово для принятия переменного числа аргументов:

[TestClass]
public class ParamsExample
{
    [TestMethod]
    [DataRow(1, 2, 3, 4)]
    [DataRow(10, 20)]
    [DataRow(5)]
    public void TestWithParams(params int[] values)
    {
        Assert.IsTrue(values.Length > 0);
    }
}

Пользовательские отображаемые имена

DisplayName Задайте свойство, чтобы настроить способ отображения тестовых вариантов в обозревателе тестов:

[TestClass]
public class DisplayNameExample
{
    [TestMethod]
    [DataRow(1, 2, DisplayName = "Functional Case FC100.1")]
    [DataRow(3, 4, DisplayName = "Edge case: small numbers")]
    public void TestMethod(int i, int j)
    {
        Assert.IsTrue(i < j);
    }
}

Подсказка

Для получения большего контроля над метаданными теста рекомендуется использовать TestDataRow<T> с DynamicData. TestDataRow<T> поддерживает отображение имени, категории тестов и игнорирование сообщений для отдельных тестовых случаев.

Игнорировать конкретные тестовые случаи

Начиная с MSTest версии 3.8 используйте IgnoreMessage свойство для пропуска определенных строк данных:

[TestClass]
public class IgnoreDataRowExample
{
    [TestMethod]
    [DataRow(1, 2)]
    [DataRow(3, 4, IgnoreMessage = "Temporarily disabled - bug #123")]
    [DataRow(5, 6)]
    public void TestMethod(int i, int j)
    {
        // Only the first and third data rows run
        // The second is skipped with the provided message
    }
}

DynamicDataAttribute

Это DynamicDataAttribute позволяет предоставлять тестовые данные из методов, свойств или полей. Используйте этот атрибут, если тестовые данные являются сложными, вычисляются динамически или слишком подробными для встроенных DataRow атрибутов.

Поддерживаемые типы источников данных

Источник данных может возвращать любой IEnumerable<T>, где T — один из типов, перечисленных в следующей таблице. Любая коллекция, реализующая IEnumerable<T>, будет работать, включая такие массивы, как List<T>, T[] или пользовательские типы коллекций. Выберите в зависимости от ваших потребностей:

Тип возвращаемого значения Безопасность типов Поддержка метаданных Лучше всего подходит для
ValueTuple (например, (int, string)) Время компиляции нет Большинство сценариев — простой синтаксис с полной проверкой типов
Tuple<...> Время компиляции нет Когда вы не можете использовать ValueTuple
TestDataRow<T> Время компиляции Да Тестовые случаи, которым нужны отображаемые имена, категории или сообщения об игнорировании
object[] Только среда выполнения нет Устаревший код— избегайте новых тестов

Подсказка

Для новых методов тестовых данных используйте ValueTuple в простых случаях или TestDataRow<T> при необходимости метаданные. Избегайте object[], так как не хватает проверки типов на этапе компиляции и это может привести к ошибкам во время выполнения из-за несоответствий типов.

Источники данных

Источник данных может быть методом, свойством или полем. Все три являются взаимозаменяемыми— выбор на основе ваших предпочтений:

[TestClass]
public class DynamicDataExample
{
    // Method - best for computed or yielded data
    public static IEnumerable<(int Value, string Name)> GetTestData()
    {
        yield return (1, "first");
        yield return (2, "second");
    }

    // Property - concise for static data
    public static IEnumerable<(int Value, string Name)> TestDataProperty =>
    [
        (1, "first"),
        (2, "second")
    ];

    // Field - simplest for static data
    public static IEnumerable<(int Value, string Name)> TestDataField =
    [
        (1, "first"),
        (2, "second")
    ];

    [TestMethod]
    [DynamicData(nameof(GetTestData))]
    public void TestWithMethod(int value, string name)
    {
        Assert.IsTrue(value > 0);
    }

    [TestMethod]
    [DynamicData(nameof(TestDataProperty))]
    public void TestWithProperty(int value, string name)
    {
        Assert.IsTrue(value > 0);
    }

    [TestMethod]
    [DynamicData(nameof(TestDataField))]
    public void TestWithField(int value, string name)
    {
        Assert.IsTrue(value > 0);
    }
}

Замечание

Методы источника данных, свойства и поля должны быть public static и возвращать IEnumerable<T> поддерживаемый тип.

Подсказка

Связанный анализатор: MSTEST0018 проверяет, существует ли источник данных, доступен и имеет правильную подпись.

Источник данных из другого класса

Укажите другой класс с помощью параметра типа:

public class TestDataProvider
{
    public static IEnumerable<(int, string)> GetTestData()
    {
        yield return (1, "first");
        yield return (2, "second");
    }
}

[TestClass]
public class DynamicDataExternalExample
{
    [TestMethod]
    [DynamicData(nameof(TestDataProvider.GetTestData), typeof(TestDataProvider))]
    public void TestMethod(int value1, string value2)
    {
        Assert.IsTrue(value1 > 0);
    }
}

Пользовательские отображаемые имена

Настройка отображаемых имен тестового случая с помощью свойства DynamicDataDisplayName.

using System.Reflection;

[TestClass]
public class DynamicDataDisplayNameExample
{
    [TestMethod]
    [DynamicData(nameof(GetTestData), DynamicDataDisplayName = nameof(GetDisplayName))]
    public void TestMethod(int value1, string value2)
    {
        Assert.IsTrue(value1 > 0);
    }

    public static IEnumerable<(int, string)> GetTestData()
    {
        yield return (1, "first");
        yield return (2, "second");
    }

    public static string GetDisplayName(MethodInfo methodInfo, object[] data)
    {
        return $"{methodInfo.Name} with value {data[0]} and '{data[1]}'";
    }
}

Замечание

Метод отображаемого имени должен быть public static, возвращать string и принимать два параметра: MethodInfo и object[].

Подсказка

Чтобы упростить подход к пользовательским отображаемым именам, рекомендуется использовать TestDataRow<T> со своим DisplayName свойством вместо отдельного метода.

Игнорировать все тестовые случаи из источника данных

Начиная с MSTest версии 3.8, используйте IgnoreMessage для пропуска всех тестовых вариантов:

[TestClass]
public class IgnoreDynamicDataExample
{
    [TestMethod]
    [DynamicData(nameof(GetTestData), IgnoreMessage = "Feature not ready")]
    public void TestMethod(int value1, string value2)
    {
        // All test cases from GetTestData are skipped
    }

    public static IEnumerable<(int, string)> GetTestData()
    {
        yield return (1, "first");
        yield return (2, "second");
    }
}

Подсказка

Чтобы игнорировать отдельные тестовые случаи, используйте TestDataRow<T> с его свойством IgnoreMessage. См. раздел TestDataRow<T> .

TestDataRow

Класс TestDataRow<T> обеспечивает расширенный контроль над тестируемыми данными в тестах на основе данных. Используйте IEnumerable<T> и TestDataRow<T> в качестве типа возвращаемого значения источника данных для указания.

  • Пользовательские отображаемые имена: установите уникальное отображаемое имя для каждого тестового примера, используя свойство DisplayName.
  • Категории тестов: присоединение метаданных к отдельным тестовых случаям с помощью TestCategories свойства.
  • Игнорировать сообщения: пропускать определенные тестовые случаи с указанием причин, используя свойство IgnoreMessage.
  • Типобезопасные данные: используйте универсальные шаблоны для строго типизированных тестовых данных.

Базовое использование

[TestClass]
public class TestDataRowExample
{
    [TestMethod]
    [DynamicData(nameof(GetTestDataRows))]
    public void TestMethod(int value1, string value2)
    {
        Assert.IsTrue(value1 > 0);
    }

    public static IEnumerable<TestDataRow<(int, string)>> GetTestDataRows()
    {
        yield return new TestDataRow<(int, string)>((1, "first"))
        {
            DisplayName = "Test Case 1: Basic scenario",
        };

        yield return new TestDataRow<(int, string)>((2, "second"))
        {
            DisplayName = "Test Case 2: Edge case",
            TestCategories = ["HighPriority", "Critical"],
        };

        yield return new TestDataRow<(int, string)>((3, "third"))
        {
            IgnoreMessage = "Not yet implemented",
        };
    }
}

DataSourceAttribute

Замечание

DataSource доступен только в .NET Framework. Для проектов .NET (Core) используйте DataRow или DynamicData вместо этого.

Подключение тестов к внешним источникам данных, таким как CSV-файлы, XML-файлы или базы данных, выполняется с помощью DataSourceAttribute.

Подробные сведения см. в статье:

ITestDataSource

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

Члены интерфейса

Интерфейс определяет два метода:

Метод Цель
GetData(MethodInfo) Возвращает тестовые данные как IEnumerable<object?[]>
GetDisplayName(MethodInfo, object?[]?) Возвращает отображаемое имя для тестового случая

Создание настраиваемого атрибута источника данных

Чтобы создать пользовательский источник данных, определите класс атрибута, наследующий от Attribute и реализующий ITestDataSource:

using System.Globalization;
using System.Reflection;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class MyDataSourceAttribute : Attribute, ITestDataSource
{
    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        // Return test data based on your custom logic
        yield return [1, "first"];
        yield return [2, "second"];
        yield return [3, "third"];
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        return data is null
            ? null
            : string.Format(CultureInfo.CurrentCulture, "{0} ({1})", methodInfo.Name, string.Join(",", data));
    }
}

[TestClass]
public class CustomDataSourceExample
{
    [TestMethod]
    [MyDataSource]
    public void TestWithCustomDataSource(int value, string name)
    {
        Assert.IsTrue(value > 0);
        Assert.IsNotNull(name);
    }
}

Практический пример: тестовые данные, зависящие от среды

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

using System.Globalization;
using System.Reflection;

[AttributeUsage(AttributeTargets.Method)]
public class TargetFrameworkDataAttribute : Attribute, ITestDataSource
{
    private readonly string[] _frameworks;

    public TargetFrameworkDataAttribute(params string[] frameworks)
    {
        _frameworks = frameworks;
    }

    public IEnumerable<object?[]> GetData(MethodInfo methodInfo)
    {
        bool isWindows = OperatingSystem.IsWindows();

        foreach (string framework in _frameworks)
        {
            // Skip .NET Framework on non-Windows platforms
            if (!isWindows && framework.StartsWith("net4", StringComparison.Ordinal))
            {
                continue;
            }

            yield return [framework];
        }
    }

    public string? GetDisplayName(MethodInfo methodInfo, object?[]? data)
    {
        return data is null
            ? null
            : string.Format(CultureInfo.CurrentCulture, "{0} ({1})", methodInfo.Name, data[0]);
    }
}

[TestClass]
public class CrossPlatformTests
{
    [TestMethod]
    [TargetFrameworkData("net48", "net8.0", "net9.0")]
    public void TestOnMultipleFrameworks(string targetFramework)
    {
        // Test runs once per applicable framework
        Assert.IsNotNull(targetFramework);
    }
}

Подсказка

В простых случаях, когда вам просто нужно настроить отображаемые имена или добавить метаданные, используйте TestDataRow<T> с DynamicData вместо того, чтобы реализовывать ITestDataSource. Резервируйте особые ITestDataSource реализации для сценариев, требующих динамической фильтрации или сложной логики генерации данных.

Развертывание стратегии

Атрибуты теста на основе данных поддерживают TestDataSourceUnfoldingStrategy свойство, которое определяет, как тестовые случаи отображаются в обозревателе тестов и результатах TRX. Это свойство также определяет, можно ли запускать отдельные тестовые случаи независимо.

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

Стратегия Поведение
Auto (по умолчанию) MSTest определяет развертывание оптимальной стратегии
Unfold Все тестовые случаи развертываются и отображаются по отдельности
Fold Все тестовые случаи свернуты в один тестовый узел

Когда изменить стратегию

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

  • Недетерминированные источники данных
  • Известные ограничения или ошибки в MSTest
  • Проблемы с производительностью во многих тестовых случаях

Пример использования

[TestClass]
public class UnfoldingExample
{
    [TestMethod(UnfoldingStrategy = TestDataSourceUnfoldingStrategy.Unfold)] // That's the default behavior
    [DataRow(1, "one")]
    [DataRow(2, "two")]
    [DataRow(3, "three")]
    public void TestMethodWithUnfolding(int value, string text)
    {
        // Each test case appears individually in Test Explorer
    }

    [TestMethod(UnfoldingStrategy = TestDataSourceUnfoldingStrategy.Fold)]
    [DataRow(1, "one")]
    [DataRow(2, "two")]
    [DataRow(3, "three")]
    public void TestMethodWithFolding(int value, string text)
    {
        // All test cases appear as a single collapsed node
    }
}

Лучшие практики

  1. Выберите правильный атрибут: используйте DataRow для простых встроенных данных. Используется DynamicData для сложных или вычисляемых данных.

  2. Назовите тестовые случаи: используйте DisplayName для упрощения идентификации тестовых сбоев.

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

  4. Используйте значимые данные: выберите тестовые данные, которые охватывают крайние случаи и пограничные условия.

  5. Рассмотрим комбинаторное тестирование. Для тестирования сочетаний параметров используйте пакет Combinatorial.MSTest .

См. также