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


Создание тестовой платформы

В этой статье объясняется, как создать пользовательскую платформу тестирования для Microsoft.Testing.Platform. Платформа тестирования является единственным обязательным расширением. Он обнаруживает и выполняет тесты и сообщает результаты обратно на платформу.

Полные общие сведения о точке расширения и концепции внутри процесса и вне процесса см. в разделе "Создание пользовательских расширений".

Если вы переносите существующий тестовый фреймворк на основе VSTest, реализация интерфейса ITestFramework напрямую является рекомендуемым подходом. Расширение VSTest Bridge доступно как переходный шаг, но собственная реализация обеспечивает лучший интерфейс.

Расширение платформы тестирования

Платформа тестирования — это основное расширение, которое предоставляет платформу тестирования с возможностью обнаружения и выполнения тестов. Платформа тестирования отвечает за передачу результатов тестов обратно на платформу тестирования. Платформа тестирования является единственным обязательным расширением, необходимым для выполнения сеанса тестирования.

Регистрация платформы тестирования

В этом разделе объясняется, как зарегистрировать тестовый фреймворк на платформе тестирования. Вы регистрируете только одну платформу тестирования для построителя тестовых приложений с помощью TestApplication.RegisterTestFramework API, как показано в документации по архитектуре Microsoft.Testing.Platform .

API регистрации определяется следующим образом:

ITestApplicationBuilder RegisterTestFramework(
    Func<IServiceProvider, ITestFrameworkCapabilities> capabilitiesFactory,
    Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework> adapterFactory);

API RegisterTestFramework требует две фабрики.

  1. Func<IServiceProvider, ITestFrameworkCapabilities>: это делегат, который принимает объект, реализующий интерфейс IServiceProvider, и возвращает объект, реализующий интерфейс ITestFrameworkCapabilities. IServiceProvider предоставляет доступ к службам платформы, таким как конфигурации, средства ведения журнала и аргументы командной строки.

    Интерфейс ITestFrameworkCapabilities используется для объявления возможностей, поддерживаемых платформой тестирования и расширениями. Он позволяет платформе и расширениям правильно взаимодействовать путем реализации и поддержки конкретных действий. Дополнительные сведения о концепции возможностейсм. в соответствующем разделе.

  2. Func<ITestFrameworkCapabilities, IServiceProvider, ITestFramework>. Это делегат, который принимает объект ITestFrameworkCapabilities, являющийся экземпляром, возвращаемым методом Func<IServiceProvider, ITestFrameworkCapabilities>, и объект IServiceProvider для повторного предоставления доступа к услугам платформы. Ожидаемый возвращаемый объект — это объект, реализующий интерфейс ITestFramework. ITestFramework служит подсистемой выполнения, которая обнаруживает и выполняет тесты, а затем передает результаты обратно на платформу тестирования.

Необходимость разделения платформы на создание ITestFrameworkCapabilities и создание ITestFramework — это оптимизация, чтобы избежать создания платформы тестирования, если поддерживаемые возможности недостаточно для выполнения текущего сеанса тестирования.

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

internal class TestingFrameworkCapabilities : ITestFrameworkCapabilities
{
    public IReadOnlyCollection<ITestFrameworkCapability> Capabilities => [];
}

internal class TestingFramework : ITestFramework
{
   public TestingFramework(ITestFrameworkCapabilities capabilities, IServiceProvider serviceProvider)
   {
       // ...
   }
   // Omitted for brevity...
}

public static class TestingFrameworkExtensions
{
    public static void AddTestingFramework(this ITestApplicationBuilder builder)
    {
        builder.RegisterTestFramework(
            _ => new TestingFrameworkCapabilities(),
            (capabilities, serviceProvider) => new TestingFramework(capabilities, serviceProvider));
    }
}

// ...

Теперь рассмотрим соответствующую точку входа этого примера с кодом регистрации:

var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);
// Register the testing framework
testApplicationBuilder.AddTestingFramework();
using var testApplication = await testApplicationBuilder.BuildAsync();
return await testApplication.RunAsync();

Замечание

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

Создание платформы тестирования

Microsoft.Testing.Platform.Extensions.TestFramework.ITestFramework реализуется расширениями, предоставляющими тестовую платформу:

public interface ITestFramework : IExtension
{
    Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context);
    Task ExecuteRequestAsync(ExecuteRequestContext context);
    Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context);
}

Интерфейс IExtension

Интерфейс ITestFramework наследует от интерфейса IExtension, который является интерфейсом, от которого наследуются все точки расширения. IExtension используется для получения имени и описания расширения. IExtension также предоставляет способ динамического включения или отключения расширения в настройке с помощью Task<bool> IsEnabledAsync(). Убедитесь, что вы возвращаете true из этого метода, если у вас нет конкретной необходимости для его отключения.

Метод CreateTestSessionAsync

Метод CreateTestSessionAsync вызывается в начале тестового сеанса и используется для инициализации платформы тестирования. API принимает объект CreateTestSessionContext и возвращает CreateTestSessionResult.

public sealed class CreateTestSessionContext : TestSessionContext
{
    public CancellationToken CancellationToken { get; }
}

Свойство наследуется от SessionUid (см. TestSessionContextраздел TestSessionContext). CancellationToken используется для остановки выполнения CreateTestSessionAsync.

Возвращаемый объект — это CreateTestSessionResult:

public sealed class CreateTestSessionResult
{
    public string? WarningMessage { get; set; }
    public string? ErrorMessage { get; set; }
    public bool IsSuccess { get; set; }
}

Свойство IsSuccess используется для указания успешности создания сеанса. Когда он возвращает false, выполнение теста останавливается.

Метод CloseTestSessionAsync

Метод CloseTestSessionAsync сопоставляется с CreateTestSessionAsync в функциональных возможностях, при этом единственное различие заключается в именах объектов. Дополнительные сведения см. в разделе CreateTestSessionAsync.

Метод ExecuteRequestAsync

Метод ExecuteRequestAsync принимает объект типа ExecuteRequestContext. Этот объект, как следует из его имени, содержит конкретные сведения о действии, которое, как ожидается, должна выполнить тестовая платформа. Определение ExecuteRequestContext:

public sealed class ExecuteRequestContext
{
    public IRequest Request { get; }
    public IMessageBus MessageBus { get; }
    public CancellationToken CancellationToken { get; }
    public void Complete();
}

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

Схема последовательности, представляющая жизненный цикл платформы тестирования.

На предыдущей схеме показано, что платформа тестирования выдает три запроса после создания экземпляра тестовой платформы. Платформа тестирования обрабатывает эти запросы и использует службу IMessageBus, которая включается в сам запрос для доставки результата для каждого конкретного запроса. После обработки конкретного запроса тестовая платформа вызывает метод Complete() для него, указывая системе тестирования, что запрос был выполнен. Платформа тестирования отслеживает все отправленные запросы. После выполнения всех запросов он вызывает CloseTestSessionAsync и удаляет экземпляр (если IDisposable/IAsyncDisposable реализован). Очевидно, что запросы и их завершение могут перекрываться, обеспечивая одновременное или асинхронное выполнение запросов.

Замечание

В настоящее время платформа тестирования не отправляет перекрывающиеся запросы и ожидает завершения запроса >> перед отправкой следующей. Однако это поведение может измениться в будущем. Поддержка одновременных запросов будет определена с помощью системы возможностей .

Реализация IRequest указывает точный запрос, который необходимо выполнить. Платформа тестирования определяет тип запроса и обрабатывает его соответствующим образом. Если тип запроса не распознается, следует вызвать исключение.

Сведения о доступных запросах см. в разделе IRequest.

IMessageBus. Эта служба, связанная с запросом, позволяет тестовой платформе асинхронно публиковать сведения о текущем запросе на платформу тестирования. Шина сообщений служит центральным узлом платформы, обеспечивая асинхронную связь между всеми компонентами и расширениями платформы. Полный список сведений, которые можно опубликовать на платформе тестирования, см. в разделе IMessageBus.

CancellationToken: этот маркер используется для прерывания обработки конкретного запроса.

Complete(). Как показано в предыдущей последовательности, метод Complete уведомляет платформу о том, что запрос был успешно обработан, и все соответствующие сведения были переданы в IMessageBus.

Предупреждение

Отсутствие вызова Complete() в запросе приведет к полному зависанию тестового приложения.

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

Обработка запросов

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

Прежде чем перейти к следующему разделу, важно тщательно понять концепцию IMessageBus, которая является важной службой для передачи сведений о выполнении тестов на платформу тестирования.

TestSessionContext

TestSessionContext является общим свойством во всех запросах, предоставляя сведения о текущем тестовом сеансе:

public class TestSessionContext
{
    public SessionUid SessionUid { get; }
}

public readonly struct SessionUid(string value)
{
    public string Value { get; }
}

TestSessionContext состоит из SessionUid, который является уникальным идентификатором текущего тестового сеанса, помогающим для ведения журнала и корреляции данных тестового сеанса.

Запрос на выполнение теста Discover

public class DiscoverTestExecutionRequest
{
    public TestSessionContext Session { get; }
    public ITestExecutionFilter Filter { get; }
}

DiscoverTestExecutionRequest предписывает платформе тестирования обнаружить тесты и сообщить об этой информации IMessageBus.

Как описано в предыдущем разделе, свойство для обнаруженного теста DiscoveredTestNodeStateProperty. Ниже приведен универсальный фрагмент кода для ссылки:

var testNode = new TestNode
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        DiscoveredTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        discoverTestExecutionRequest.Session.SessionUid,
        testNode));

// ...

ЗапросНаВыполнениеТеста

public class RunTestExecutionRequest
{
    public TestSessionContext Session { get; }
    public ITestExecutionFilter Filter { get; }
}

RunTestExecutionRequest указывает платформе тестирования выполнить тесты и сообщить об этой информации IMessageBus.

Ниже приведен универсальный фрагмент кода для ссылки:

var skippedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        SkippedTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        skippedTestNode));

// ...

var successfulTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        PassedTestNodeStateProperty.CachedInstance),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        successfulTestNode));

// ...

var assertionFailedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        new FailedTestNodeStateProperty(assertionException)),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        assertionFailedTestNode));

// ...

var failedTestNode = new TestNode()
{
    Uid = GenerateUniqueStableId(),
    DisplayName = GetDisplayName(),
    Properties = new PropertyBag(
        new ErrorTestNodeStateProperty(ex.InnerException!)),
};

await context.MessageBus.PublishAsync(
    this,
    new TestNodeUpdateMessage(
        runTestExecutionRequest.Session.SessionUid,
        failedTestNode));

Данные TestNodeUpdateMessage

Как упоминалось в разделе IMessageBus, прежде чем использовать шину сообщений, необходимо указать тип данных, которые вы планируете предоставить. Платформа тестирования определила хорошо известный тип TestNodeUpdateMessageдля представления концепции информации об обновлении теста.

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

public sealed class TestNodeUpdateMessage(
    SessionUid sessionUid,
    TestNode testNode,
    TestNodeUid? parentTestNodeUid = null)
{
    public TestNode TestNode { get; }
    public TestNodeUid? ParentTestNodeUid { get; }
}

public class TestNode
{
    public required TestNodeUid Uid { get; init; }
    public required string DisplayName { get; init; }
    public PropertyBag Properties { get; init; } = new();
}

public sealed class TestNodeUid(string value);

public sealed partial class PropertyBag
{
    public PropertyBag();
    public PropertyBag(params IProperty[] properties);
    public PropertyBag(IEnumerable<IProperty> properties);
    public int Count { get; }
    public void Add(IProperty property);
    public bool Any<TProperty>();
    public TProperty? SingleOrDefault<TProperty>();
    public TProperty Single<TProperty>();
    public TProperty[] OfType<TProperty>();
    public IEnumerable<IProperty> AsEnumerable();
    public IEnumerator<IProperty> GetEnumerator();
    ...
}

public interface IProperty
{
}
  • TestNodeUpdateMessage: TestNodeUpdateMessage состоит из двух свойств: TestNode и ParentTestNodeUid. ParentTestNodeUid указывает, что тест может иметь родительский тест, вводя концепцию дерева тестов , где TestNodeмогут быть сопоставлены друг с другом. Эта структура позволяет в будущем осуществлять улучшения и добавлять функции на основе дерева и связи между узлами. Если ваша платформа тестирования не требует структуры дерева тестов, вы можете не использовать ее и просто задать значение null, что приведет к простому плоскому списку TestNode.

  • TestNode: TestNode состоит из трех свойств, один из которых является Uid типа TestNodeUid. Этот Uid служит в качестве уникального стабильного идентификатора для узла. Термин UNIQUE STABLE ID означает, что один и тот же TestNode должен поддерживать ИДЕНТИЧНЫЙUid при различных запусках и в разных операционных системах. TestNodeUid — это произвольная непрозрачная строка, которую платформа тестирования принимает как есть.

Это важно

Стабильность и уникальность идентификатора имеют решающее значение в домене тестирования. Они обеспечивают точное назначение одного теста для выполнения и позволяют идентификатору служить в качестве постоянного идентификатора для теста, упрощая мощные расширения и функции.

Второе свойство — это DisplayName, которое является удобным для восприятия человеком названием для теста. Например, это имя отображается при выполнении команды --list-tests в командной строке.

Третий атрибут — Properties, который является типом PropertyBag. Как показано в коде, это специализированный контейнер свойств, содержащий универсальные свойства для TestNodeUpdateMessage. Это означает, что можно добавить любое свойство к узлу, реализующему интерфейс заполнителя IProperty.

Платформа тестирования выявляет специфические свойства, добавленные в TestNode.Properties, чтобы определить, прошел ли тест, провален или пропущен.

Текущий список доступных свойств можно найти с относительным описанием в разделе TestNodeUpdateMessage.TestNode.

Тип PropertyBag обычно доступен в каждой IData и используется для хранения других свойств, которые могут запрашиваться платформой и расширениями. Этот механизм позволяет нам улучшать платформу новой информацией, не внося критических изменений. Если компонент распознает свойство, он может запросить его; в противном случае он будет игнорировать его.

Наконец, в этом разделе чётко поясняется, что для реализации тестовой платформы необходимо внедрить IDataProducer, который создаёт TestNodeUpdateMessage, как в приведённом ниже примере.

internal sealed class TestingFramework
    : ITestFramework, IDataProducer
{
   // ...

   public Type[] DataTypesProduced =>
   [
       typeof(TestNodeUpdateMessage)
   ];

   // ...
}

Если для адаптера тестирования требуется публикация файлов во время выполнения, можно найти распознанные свойства в этом исходном файле: https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/Messages/FileArtifacts.cs. Как видно, можно предоставить файловые активы в общем виде или связать их с определенным TestNode. Помните, что если вы планируете отправить SessionFileArtifact, необходимо заранее зарегистрировать его на платформе, как показано ниже:

internal sealed class TestingFramework
    : ITestFramework, IDataProducer
{
   // ...

   public Type[] DataTypesProduced =>
   [
       typeof(TestNodeUpdateMessage),
       typeof(SessionFileArtifact)
   ];

   // ...
}

Известные свойства

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

В этом сегменте мы рассмотрим различные известные варианты IProperty и их соответствующие последствия.

Полный список известных свойств см. в TestNodeProperties.cs. Если вы заметили, что не хватает описания свойства, сообщите о проблеме.

Эти свойства можно разделить на следующие категории:

  1. универсальные сведения: свойства, которые можно включить в любой тип запроса.
  2. сведения об обнаружении: свойства, предоставляемые во время запроса на обнаружение DiscoverTestExecutionRequest.
  3. сведения о выполнении: свойства, предоставляемые во время запроса выполнения теста RunTestExecutionRequest.

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

С другой стороны, необязательные свойства повышают возможности тестирования, предоставляя дополнительные сведения. Они особенно полезны в сценариях интегрированной среды разработки (например, VS, VSCode и т. д.), запусках консоли или при поддержке конкретных расширений, требующих более подробной информации для правильной работы. Однако эти необязательные свойства не влияют на выполнение тестов.

Замечание

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

Универсальные сведения
public record KeyValuePairStringProperty(
    string Key,
    string Value)
        : IProperty;

KeyValuePairStringProperty обозначает общие данные пары "ключ-значение".

public record struct LinePosition(
    int Line,
    int Column);

public record struct LinePositionSpan(
    LinePosition Start,
    LinePosition End);

public abstract record FileLocationProperty(
    string FilePath,
    LinePositionSpan LineSpan)
        : IProperty;

public sealed record TestFileLocationProperty(
    string FilePath,
    LinePositionSpan LineSpan)
        : FileLocationProperty(FilePath, LineSpan);

TestFileLocationProperty используется для определения расположения теста в исходном файле. Это особенно полезно, если инициатором является интегрированная среда разработки, например Visual Studio или Visual Studio Code.

public sealed record TestMethodIdentifierProperty(
    string AssemblyFullName,
    string Namespace,
    string TypeName,
    string MethodName,
    string[] ParameterTypeFullNames,
    string ReturnTypeFullName)

TestMethodIdentifierProperty — уникальный идентификатор для метода тестирования.

public sealed record TestMetadataProperty(
    string Key,
    string Value)

TestMetadataProperty используется для передачи характеристик или признаков TestNode.

Сведения об обнаружении
public sealed record DiscoveredTestNodeStateProperty(
    string? Explanation = null)
{
    public static DiscoveredTestNodeStateProperty CachedInstance { get; }
}

DiscoveredTestNodeStateProperty указывает, что этот testNode обнаружен. Он используется при отправке DiscoverTestExecutionRequest в тестовый фреймворк. Обратите внимание на удобное кэшированное значение, предлагаемое свойством CachedInstance. Это свойство обязательно.

Сведения о выполнении
public sealed record InProgressTestNodeStateProperty(
    string? Explanation = null)
{
    public static InProgressTestNodeStateProperty CachedInstance { get; }
}

InProgressTestNodeStateProperty сообщает платформе тестирования, что выполнение TestNode запланировано и в настоящее время происходит. Обратите внимание на удобное кэшированное значение, предлагаемое свойством CachedInstance.

public readonly record struct TimingInfo(
    DateTimeOffset StartTime,
    DateTimeOffset EndTime,
    TimeSpan Duration);

public sealed record StepTimingInfo(
    string Id,
    string Description,
    TimingInfo Timing);

public sealed record TimingProperty : IProperty
{
    public TimingProperty(TimingInfo globalTiming)
        : this(globalTiming, [])
    {
    }

    public TimingProperty(
        TimingInfo globalTiming,
        StepTimingInfo[] stepTimings)
    {
        GlobalTiming = globalTiming;
        StepTimings = stepTimings;
    }

    public TimingInfo GlobalTiming { get; }

    public StepTimingInfo[] StepTimings { get; }
}

TimingProperty используется для ретрансляции сведений о времени выполнения TestNode. Кроме того, он позволяет задавать время выполнения отдельных шагов с помощью StepTimingInfo. Это особенно полезно, если концепция теста разделена на несколько этапов, таких как инициализация, выполнение и очистка.

Один и только один из следующих свойств требуется вместо TestNode и передает результат TestNode на платформу тестирования.

public sealed record PassedTestNodeStateProperty(
    string? Explanation = null)
        : TestNodeStateProperty(Explanation)
{
    public static PassedTestNodeStateProperty CachedInstance
        { get; } = new PassedTestNodeStateProperty();
}

PassedTestNodeStateProperty сообщает платформе тестирования, что этот TestNode пройден. Обратите внимание на удобное кэшированное значение, предлагаемое свойством CachedInstance.

public sealed record SkippedTestNodeStateProperty(
    string? Explanation = null)
        : TestNodeStateProperty(Explanation)
{
    public static SkippedTestNodeStateProperty CachedInstance
        { get; } =  new SkippedTestNodeStateProperty();
}

SkippedTestNodeStateProperty информирует платформу тестирования, что этот TestNode был пропущен. Обратите внимание на удобное кэшированное значение, предлагаемое свойством CachedInstance.

public sealed record FailedTestNodeStateProperty : TestNodeStateProperty
{
    public FailedTestNodeStateProperty()
        : base(default(string))
    {
    }

    public FailedTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public FailedTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
        : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

FailedTestNodeStateProperty сообщает платформе тестирования, что эта TestNode завершается с ошибкой после проверки.

public sealed record ErrorTestNodeStateProperty : TestNodeStateProperty
{
    public ErrorTestNodeStateProperty()
        : base(default(string))
    {
    }

    public ErrorTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public ErrorTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
            : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

ErrorTestNodeStateProperty сообщает платформе тестирования, что эта TestNode завершилась ошибкой. Этот тип сбоя отличается от FailedTestNodeStateProperty, который используется для ошибок утверждения. Например, можно сообщить о проблемах, таких как ошибки инициализации теста с помощью ErrorTestNodeStateProperty.

public sealed record TimeoutTestNodeStateProperty : TestNodeStateProperty
{
    public TimeoutTestNodeStateProperty()
        : base(default(string))
    {
    }

    public TimeoutTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public TimeoutTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
            : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }

    public TimeSpan? Timeout { get; init; }
}

TimeoutTestNodeStateProperty сообщает платформе тестирования, что эта TestNode провалена из-за тайм-аута. Вы можете сообщить о времени ожидания с помощью параметра Timeout.

public sealed record CancelledTestNodeStateProperty : TestNodeStateProperty
{
    public CancelledTestNodeStateProperty()
        : base(default(string))
    {
    }

    public CancelledTestNodeStateProperty(string explanation)
        : base(explanation)
    {
    }

    public CancelledTestNodeStateProperty(
        Exception exception,
        string? explanation = null)
        : base(explanation ?? exception.Message)
    {
        Exception = exception;
    }

    public Exception? Exception { get; }
}

CancelledTestNodeStateProperty сообщает платформе тестирования, что TestNode не выполнен из-за отмены.