Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье рассматриваются точки расширяемости для MTP за пределами самой тестовой платформы. Сведения о создании платформы тестирования см. в разделе "Создание тестовой платформы".
Полные общие сведения о точке расширения и концепции внутри процесса и вне процесса см. в разделе "Создание пользовательских расширений".
Точки расширяемости
Платформа тестирования предоставляет дополнительные точки расширяемости, позволяющие настраивать поведение платформы и платформы тестирования. Эти точки расширяемости являются необязательными и могут использоваться для улучшения возможностей тестирования.
Tip
Каждое расширение, показанное в этой статье, содержит фрагмент кода регистрации вручную (например, builder.TestHost.AddDataConsumer(...)). Если вы распространяете своё расширение в виде пакета NuGet, вы можете дать пользователям возможность избежать ручного вызова, предоставив TestingPlatformBuilderHook и небольшой файл свойств MSBuild (props). Автоматически сгенерированная точка входа затем автоматически вызовет ваш хук. Подробнее см. Автоматическая регистрация вашего расширения с помощью TestingPlatformBuilderHook.
Расширения ICommandLineOptionsProvider
Заметка
При расширении этого API настраиваемое расширение будет существовать как в процессе тестового узла, так и вне него.
Как описано в разделе архитектуры , начальный шаг включает создание ITestApplicationBuilder для регистрации тестовой среды и расширений в нем.
var builder = await TestApplication.CreateBuilderAsync(args);
Метод CreateBuilderAsync принимает массив строк (string[]) с именем args. Эти аргументы можно использовать для передачи параметров командной строки всем компонентам платформы тестирования (включая встроенные компоненты, платформы тестирования и расширения), что позволяет настраивать их поведение.
Как правило, переданные аргументы являются теми, которые получены в стандартном методе Main(string[] args). Однако если среда размещения отличается, можно указать любой список аргументов.
Аргументы должны быть префиксированы с двойным --тире. Например: --filter.
Если компонент, например платформа тестирования или точка расширения, хочет предложить настраиваемые параметры командной строки, это можно сделать, реализуя интерфейс ICommandLineOptionsProvider. Затем эту реализацию можно зарегистрировать в ITestApplicationBuilder через фабрику регистрации свойства CommandLine, как показано ниже.
builder.CommandLine.AddProvider(
static () => new CustomCommandLineOptions());
В приведенном примере CustomCommandLineOptions является реализацией интерфейса ICommandLineOptionsProvider, этот интерфейс состоит из следующих элементов и типов данных:
public interface ICommandLineOptionsProvider : IExtension
{
IReadOnlyCollection<CommandLineOption> GetCommandLineOptions();
Task<ValidationResult> ValidateOptionArgumentsAsync(
CommandLineOption commandOption,
string[] arguments);
Task<ValidationResult> ValidateCommandLineOptionsAsync(
ICommandLineOptions commandLineOptions);
}
public sealed class CommandLineOption
{
public string Name { get; }
public string Description { get; }
public ArgumentArity Arity { get; }
public bool IsHidden { get; }
// ...
}
public interface ICommandLineOptions
{
bool IsOptionSet(string optionName);
bool TryGetOptionArgumentList(
string optionName,
out string[]? arguments);
}
Как отмечалось, ICommandLineOptionsProvider расширяет интерфейс IExtension. Таким образом, как и любое другое расширение, можно включить или отключить его с помощью API IExtension.IsEnabledAsync.
Порядок выполнения ICommandLineOptionsProvider:
Рассмотрим api и их среднее значение:
ICommandLineOptionsProvider.GetCommandLineOptions(): этот метод используется для получения всех параметров, предлагаемых компонентом. Каждому CommandLineOption требуется указать следующие свойства:
string name: это имя параметра, представленное без дефиса. Например, фильтра будут использоваться пользователями в качестве --filter.
string description: это описание параметра. Он будет отображаться, когда пользователи передают --help в качестве аргумента построителю приложений.
ArgumentArity arity: Арность параметра — это количество значений, которые можно передать, если указан этот параметр или команда. Доступные степени (арности) в текущий момент:
-
Zero: обозначает арность аргумента равную нулю. -
ZeroOrOne: представляет арность аргумента ноль или один. -
ZeroOrMore: представляет арность аргумента ноль или более. -
OneOrMore: представляет арность аргумента, равную одному или более. -
ExactlyOne: представляет арность аргумента ровно одного.
Например, см. таблицу arity System.CommandLine .
bool isHidden: это свойство означает, что параметр доступен для использования, но не будет отображаться в описании при вызове --help.
ICommandLineOptionsProvider.ValidateOptionArgumentsAsync. Этот метод используется для проверки аргумента, предоставленного пользователем.
Например, если у вас есть параметр с именем --dop, представляющий степень параллелизма для нашей пользовательской платформы тестирования, пользователь может ввести --dop 0. В этом сценарии значение 0 будет недопустимым, так как ожидается, что он имеет степень параллелизма 1 или более. С помощью ValidateOptionArgumentsAsyncможно выполнить предварительные проверки и при необходимости вернуть сообщение об ошибке.
Возможной реализацией для приведенного выше примера может быть:
public Task<ValidationResult> ValidateOptionArgumentsAsync(
CommandLineOption commandOption,
string[] arguments)
{
if (commandOption.Name == "dop")
{
if (!int.TryParse(arguments[0], out int dopValue) || dopValue <= 0)
{
return ValidationResult.InvalidTask("--dop must be a positive integer");
}
}
return ValidationResult.ValidTask;
}
ICommandLineOptionsProvider.ValidateCommandLineOptionsAsync: этот метод вызывается как последний и позволяет выполнять глобальную проверку когерентности.
Например, предположим, что наша платформа тестирования имеет возможность создать отчет о результатах теста и сохранить его в файле. Доступ к этой функции осуществляется с помощью параметра --generatereport, а имя файла указывается с --reportfilename myfile.rep. В этом сценарии, если пользователь предоставляет только параметр --generatereport без указания имени файла, проверка должна завершиться ошибкой, так как отчет не может быть создан без имени файла.
Возможной реализацией для приведенного выше примера может быть:
public Task<ValidationResult> ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions)
{
bool generateReportEnabled = commandLineOptions.IsOptionSet(GenerateReportOption);
bool reportFileName = commandLineOptions.TryGetOptionArgumentList(ReportFilenameOption, out string[]? _);
return (generateReportEnabled || reportFileName) && !(generateReportEnabled && reportFileName)
? ValidationResult.InvalidTask("Both `--generatereport` and `--reportfilename` need to be provided simultaneously.")
: ValidationResult.ValidTask;
}
Обратите внимание, что метод ValidateCommandLineOptionsAsync предоставляет службу ICommandLineOptions, которая используется для получения сведений о аргументах, проанализированных самой платформой.
Расширения ITestSessionLifetimeHandler
ITestSessionLifetimeHandler — это расширение , которое позволяет выполнять код до начала тестового сеанса и после его завершения.
Чтобы зарегистрировать пользовательский ITestSessionLifetimeHandler, используйте следующий API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHost.AddTestSessionLifetimeHandle(
static serviceProvider => new CustomTestSessionLifetimeHandler());
Фабрика использует IServiceProvider для получения доступа к набору служб, предлагаемых платформой тестирования.
Важный
Последовательность регистрации является важной, так как API вызываются в том порядке, в который они были зарегистрированы.
Интерфейс ITestSessionLifetimeHandler включает следующие методы:
public interface ITestSessionLifetimeHandler : ITestHostExtension
{
Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext);
Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext);
}
public interface ITestSessionContext
{
SessionUid SessionUid { get; }
CancellationToken CancellationToken { get; }
}
public readonly struct SessionUid(string value)
{
public string Value { get; } = value;
}
public interface ITestHostExtension : IExtension
{
}
Важный
В версии MTP 2.0.0 оба метода были изменены и теперь принимают единственный параметр ITestSessionContext, который открывает доступ к SessionUid и CancellationToken. В MTP 1.x каждый метод принял отдельный SessionUid и CancellationToken аргумент. Дополнительные сведения см. в разделе Миграция с Microsoft.Testing.Platform (MTP) v1 на v2.
ITestSessionLifetimeHandler — это тип ITestHostExtension, который служит основой для всех расширений тестового узла. Как и все остальные точки расширения, он также наследует от IExtension. Таким образом, как и любое другое расширение, можно включить или отключить его с помощью API IExtension.IsEnabledAsync.
Рассмотрим следующие сведения для этого API:
OnTestSessionStartingAsync: Этот метод вызывается перед началом тестового сеанса и получает ITestSessionContext, чей SessionUid предоставляет непрозрачный идентификатор текущего тестового сеанса.
OnTestSessionFinishingAsync: этот метод вызывается после завершения тестового сеанса, гарантируя, что платформа тестирования завершена выполнение всех тестов и сообщила все соответствующие данные платформе. Как правило, в этом методе расширение использует IMessageBus для передачи пользовательских ресурсов или данных в общую шину платформы. Этот метод также может сигнализировать любому пользовательскому внепроцессному расширению , что тестовый сеанс завершился.
Наконец, ITestSessionContext предоставляет CancellationToken, который расширение должно соблюдать.
Если расширение требует интенсивной инициализации и необходимо использовать шаблон async/await, можно обратиться к Async extension initialization and cleanup. Если вам нужно обмениваться состоянием между точками расширения, вы можете обратиться к разделу CompositeExtensionFactory<T>.
Расширения ITestApplicationLifecycleCallbacks
Важный
ITestApplicationLifecycleCallbacks удален в MTP 2.0.0. Вместо этого используйте ITestHostApplicationLifetime. Дополнительные сведения см. в разделе Миграция с Microsoft.Testing.Platform (MTP) v1 на v2.
Интерфейс ITestHostApplicationLifetime позволяет встроенному расширению запускать код в начале и конце тестового узла.
Чтобы зарегистрировать пользовательский ITestHostApplicationLifetime, используйте следующий API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHost.AddTestHostApplicationLifetime(
static serviceProvider
=> new CustomTestHostApplicationLifetime());
Фабрика использует IServiceProvider для доступа к службам, предлагаемым платформой тестирования.
Важный
Последовательность регистрации является важной, так как API вызываются в том порядке, в который они были зарегистрированы.
Интерфейс ITestHostApplicationLifetime включает следующие методы:
public interface ITestHostApplicationLifetime : ITestHostExtension
{
Task BeforeRunAsync(CancellationToken cancellationToken);
Task AfterRunAsync(
int exitCode,
CancellationToken cancellationToken);
}
public interface ITestHostExtension : IExtension
{
}
Интерфейс ITestHostApplicationLifetime расширяет ITestHostExtension, который служит основой для всех расширений узла тестирования. Как и все остальные точки расширения, он также наследует от IExtension. Таким образом, как и любое другое расширение, можно включить или отключить его с помощью API IExtension.IsEnabledAsync.
BeforeRunAsync: этот метод служит начальной точкой контакта для тестового узла и является первой возможностью для внутреннего процесса расширения выполнения функции. Обычно он используется для установления соединения с любыми соответствующими расширениями , работающими вне процесса, если функция предназначена для работы в обеих этих средах.
Например, встроенная функция дампа зависания состоит из внутрипроцессных и внепроцессных расширений, и этот метод используется для обмена информацией с внепроцессного компонента расширения.
AfterRunAsync: этот метод является последним вызовом перед выходом из int ITestApplication.RunAsync() и предоставляет exit code. Он предназначен исключительно для задач очистки и уведомления любого соответствующего расширения , работающего вне процесса, что тестовый узел сейчас будет завершаться.
Наконец, оба API принимают CancellationToken, который расширение должно учитывать.
Расширения IDataConsumer
IDataConsumer — это внутрипроцессное расширение, способное подписываться на IData информацию, публикуемую в IMessageBusплатформой тестирования и её расширениями, и получать её.
Эта точка расширения имеет решающее значение, так как разработчики могут собирать и обрабатывать все сведения, созданные во время тестового сеанса.
Чтобы зарегистрировать кастомный IDataConsumer, используйте следующий интерфейс API.
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHost.AddDataConsumer(
static serviceProvider => new CustomDataConsumer());
Фабрика использует IServiceProvider для получения доступа к набору служб, предлагаемых платформой тестирования.
Важный
Последовательность регистрации является важной, так как API вызываются в том порядке, в который они были зарегистрированы.
Интерфейс IDataConsumer включает следующие методы:
public interface IDataConsumer : IExtension
{
Type[] DataTypesConsumed { get; }
Task ConsumeAsync(
IDataProducer dataProducer,
IData value,
CancellationToken cancellationToken);
}
public interface IData
{
string DisplayName { get; }
string? Description { get; }
}
Важный
В версии MTP 2.0.0 IDataConsumer был перемещён в пространство имён Microsoft.Testing.Platform.Extensions и теперь напрямую наследуется от IExtension. В версии MTP 1.x он был расширен с помощью ITestHostExtension. Вы по-прежнему регистрируете его через builder.TestHost.AddDataConsumer(...). Дополнительные сведения см. в разделе Миграция с Microsoft.Testing.Platform (MTP) v1 на v2.
IDataConsumer наследуется от IExtension. Таким образом, как и любое другое расширение, можно включить или отключить его с помощью API IExtension.IsEnabledAsync.
DataTypesConsumed: это свойство возвращает список Type, которые это расширение планирует потреблять. Он соответствует IDataProducer.DataTypesProduced. В частности, IDataConsumer может подписаться на несколько типов, исходящих из разных экземпляров IDataProducer без каких-либо проблем.
ConsumeAsync: Этот метод вызывается всякий раз, когда в IMessageBus публикуются данные типа, на который подписан текущий потребитель. Он получает IDataProducer для предоставления сведений о производителе полезной нагрузки, а также о самой полезной нагрузке IData. Как видно, IData — это универсальный интерфейс заполнителя, содержащий общие информативные данные. Возможность публикации различных типов IData подразумевает, что потребитель должен переключаться на сам тип, чтобы привести его к правильному типу и получить доступ к определенной информации.
Пример реализации клиента, который хочет подробно разработать TestNodeUpdateMessage, созданный с помощью тестовой платформы , может быть:
internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer
{
public Type[] DataTypesConsumed => new[] { typeof(TestNodeUpdateMessage) };
...
public Task ConsumeAsync(
IDataProducer dataProducer,
IData value,
CancellationToken cancellationToken)
{
var testNodeUpdateMessage = (TestNodeUpdateMessage)value;
switch (testNodeUpdateMessage.TestNode.Properties.Single<TestNodeStateProperty>())
{
case InProgressTestNodeStateProperty _:
{
...
break;
}
case PassedTestNodeStateProperty _:
{
...
break;
}
case FailedTestNodeStateProperty failedTestNodeStateProperty:
{
...
break;
}
case SkippedTestNodeStateProperty _:
{
...
break;
}
...
}
return Task.CompletedTask;
}
...
}
Наконец, API принимает CancellationToken, которое расширение должно учитывать.
Важный
Обрабатывайте полезную нагрузку непосредственно в методе ConsumeAsync. Обычный IDataConsumer асинхронно обрабатывает данные: IMessageBus помещает каждую опубликованную полезную нагрузку в очередь и обрабатывает её в фоновом цикле, поэтому IMessageBus.PublishAsync не блокирует производителя, и нет никаких гарантий относительно того, когда ConsumeAsync выполняется по отношению к тому, как производитель продолжает свою работу. Платформа сериализует доставку таким образом, что для каждого потребителя в каждый момент времени обрабатывается только одно сообщение, что устраняет необходимость в сложной синхронизации в рамках одного потребителя.
Заметка
Для сценариев, в которых требуется гарантия того, что обработка потребителем произойдёт до того, как производитель продолжит выполнение (например, до запуска теста), в MTP 2.3.0 был представлен экспериментальный интерфейс-маркер IBlockingDataConsumer (при этом необходимо подавить диагностику TPEXP). Потребитель, который также реализует IBlockingDataConsumer, вызывается синхронно шиной сообщений: вызовы выполняются последовательно, PublishAsync блокируется до завершения ConsumeAsync, и любое исключение, возникающее в ConsumeAsync, передаётся обратно производителю, опубликовавшему данные. Шина сообщений не доставляет данные производителя обратно тому же производителю (с тем же UID), поэтому публикация с собственным UID безопасна. Тем не менее, блокирующий потребитель не должен изнутри ConsumeAsync публиковать данные, которые маршрутизируются обратно ему же под другим UID производителя, поскольку такой повторный вход приведёт к взаимоблокировке.
Предупреждение
При использовании IDataConsumer в сочетании с ITestSessionLifetimeHandler в составной точке расширенияважно игнорировать все данные, полученные после выполнения ITestSessionLifetimeHandler.OnTestSessionFinishingAsync.
OnTestSessionFinishingAsync является последней возможностью обработки накопленных данных и передачи новых сведений в IMessageBus, поэтому любые данные, потребляемые за пределами этой точки, не будут пригодны для использования расширением.
Если расширение требует интенсивной инициализации и необходимо использовать шаблон async/await, можно обратиться к Async extension initialization and cleanup. Если вам нужно обмениваться состоянием между точками расширения, вы можете обратиться к разделу CompositeExtensionFactory<T>.
Артефакты файла шины сообщений
Пользовательская полезная нагрузка IData не имеет автоматического вывода в пользовательском интерфейсе или командной строке. Платформа выводит только те данные, для которых зарегистрирован соответствующий потребитель, поэтому если вы публикуете собственный тип IData и его ничто не потребляет, ничего не выводится и не пересылается. Чтобы сделать файлы, создаваемые вашим расширением, видимыми для пользователей и инструментов, опубликуйте одно из встроенных сообщений об артефактах файлов, которое уже распознают встроенные обработчики терминала и dotnet test.
Для файлов уровня запуска или уровня сеанса опубликуйте файл FileArtifact или SessionFileArtifact. Оба были представлены в MTP 1.0.0 и находятся в пространстве имён Microsoft.Testing.Platform.Extensions.Messages:
-
FileArtifactне имеет области видимости. Используйте это для файла, который не привязан к определенному тестовому сеансу. -
SessionFileArtifactдействует в рамках запуска/сеанса через свойSessionUid. Используйте его для артефактов, созданных для всего запуска, таких как результаты покрытия, отчеты, дампы или видеозаписи.
Встроенный терминал и потребители dotnet test обрабатывают оба типа и выводят или передают дальше пути к файлам, поэтому их можно увидеть в выводе консоли и в канале dotnet test. Поскольку потребители управляют окончательным представлением, храните каждый файл на диске и обеспечивайте к нему доступ до тех пор, пока файл может быть использован или переслан; не удаляйте его в том же вызове PublishAsync.
Сам объект артефакта не несет удостоверения производителя. Шина сообщений передаёт каждому потребителю исходный IDataProducer через аргумент dataProducer у IDataConsumer.ConsumeAsync, поэтому потребители узнают, кто создал файл, из этого аргумента, а не из артефакта. Шина сообщений также не получает в своё владение указанный файл, не перемещает и не удаляет его: поставщик отвечает за жизненный цикл файла, а потребители лишь получают, считывают или передают дальше его путь.
Заметка
Регистрация сторонних компонентов IDataConsumer является общедоступной только на builder.TestHost (тестовом внутрипроцессном узле). Нет общедоступного builder.TestHostControllers.AddDataConsumer API для непроцессных потребителей. Сторонние расширения отчетов, отображающие артефакты, используют внутреннюю интеграцию платформы, на которую не могут полагаться пользовательские расширения. Чтобы отображать файлы из вашего расширения, публикуйте встроенные сообщения об артефактах, описанные здесь, и позвольте встроенным компонентам-потребителям отображать их.
Производитель, публикующий артефакт сеанса, должен реализовать IDataProducer и перечислить в DataTypesProduced точные типы сообщений времени выполнения, которые он публикует. В следующем примере по завершении сеанса публикуется отчет о охвате:
internal sealed class CoverageReportProducer(IMessageBus messageBus)
: IDataProducer, ITestSessionLifetimeHandler
{
public string Uid => nameof(CoverageReportProducer);
public string Version => "1.0.0";
public string DisplayName => "Coverage report producer";
public string Description => "Publishes the coverage report as a session artifact.";
// List the exact runtime message types this producer publishes.
public Type[] DataTypesProduced => new[] { typeof(SessionFileArtifact) };
public Task<bool> IsEnabledAsync() => Task.FromResult(true);
public Task OnTestSessionStartingAsync(ITestSessionContext context)
=> Task.CompletedTask;
public Task OnTestSessionFinishingAsync(ITestSessionContext context)
{
var report = new FileInfo("coverage.cobertura.xml");
return messageBus.PublishAsync(
this,
new SessionFileArtifact(
context.SessionUid,
report,
"Code coverage",
"Cobertura coverage report for the run."));
}
}
Чтобы прикрепить файл к конкретному тесту, чтобы терминал, dotnet test и IDE связывали его с этим тестом и отображали вместе с ним, не публикуйте отдельный файловый артефакт. Вместо этого добавьте одну или несколько записей FileArtifactProperty в TestNode, о которых сообщает ваша среда тестирования через TestNodeUpdateMessage.
FileArtifactProperty представлен в MTP 1.7.0:
var testNode = new TestNode
{
Uid = testUid,
DisplayName = testDisplayName,
Properties = new PropertyBag(
PassedTestNodeStateProperty.CachedInstance,
new FileArtifactProperty(
new FileInfo("screenshot.png"),
"Failure screenshot",
"Screenshot captured while the test ran.")),
};
await messageBus.PublishAsync(
dataProducer,
new TestNodeUpdateMessage(sessionUid, testNode));
Важный
TestNodeFileArtifact устарел и удален в MTP 2.0.0. Чтобы прикрепить файлы на уровне теста, используйте FileArtifactProperty на странице TestNode. Дополнительные сведения см. в разделе Миграция с Microsoft.Testing.Platform (MTP) v1 на v2.
Заметка
MTP 2.4.0 (ещё не выпущенная по состоянию на июль 2026 года) добавляет экспериментальную перегрузку конструктора kind и свойство Kind в FileArtifact и SessionFileArtifact (требует подавления диагностики TPEXP).
Kind представляет собой определяемый производителем идентификатор в формате reverse-DNS для формата артефакта (например, microsoft.testing.trx, microsoft.testing.junit, microsoft.testing.ctrf или microsoft.testing.html), который средства постобработки могут использовать для группирования артефактов одного формата с целью объединения. Оставьте это null или опустите, если производитель не указывает известный тип. В настоящее время Kind — это лишь контракт метаданных; не следует предполагать, что он поддерживает какие-либо широкие возможности оркестрации слияния сверх этого.
#pragma warning disable TPEXP // Experimental API.
new SessionFileArtifact(
context.SessionUid,
trxFile,
"TRX report",
"Test results in TRX format.",
kind: "microsoft.testing.trx");
#pragma warning restore TPEXP
Расширения ITestHostEnvironmentVariableProvider
ITestHostEnvironmentVariableProvider — это внепроцессное расширение , которое позволяет устанавливать пользовательские переменные среды для тестового узла. Использование этой точки расширения гарантирует, что платформа тестирования инициирует новый узел с соответствующими переменными среды, как описано в разделе архитектуры.
Чтобы зарегистрировать пользовательскую настройку ITestHostEnvironmentVariableProvider, используйте следующий API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHostControllers.AddEnvironmentVariableProvider(
static serviceProvider => new CustomEnvironmentVariableForTestHost());
Фабрика использует IServiceProvider для получения доступа к набору служб, предлагаемых платформой тестирования.
Важный
Последовательность регистрации является важной, так как API вызываются в том порядке, в который они были зарегистрированы.
Интерфейс ITestHostEnvironmentVariableProvider включает следующие методы и типы:
public interface ITestHostEnvironmentVariableProvider : ITestHostControllersExtension, IExtension
{
Task UpdateAsync(IEnvironmentVariables environmentVariables);
Task<ValidationResult> ValidateTestHostEnvironmentVariablesAsync(
IReadOnlyEnvironmentVariables environmentVariables);
}
public interface IEnvironmentVariables : IReadOnlyEnvironmentVariables
{
void SetVariable(EnvironmentVariable environmentVariable);
void RemoveVariable(string variable);
}
public interface IReadOnlyEnvironmentVariables
{
bool TryGetVariable(
string variable,
[NotNullWhen(true)] out OwnedEnvironmentVariable? environmentVariable);
}
public sealed class OwnedEnvironmentVariable : EnvironmentVariable
{
public IExtension Owner { get; }
public OwnedEnvironmentVariable(
IExtension owner,
string variable,
string? value,
bool isSecret,
bool isLocked);
}
public class EnvironmentVariable
{
public string Variable { get; }
public string? Value { get; }
public bool IsSecret { get; }
public bool IsLocked { get; }
}
ITestHostEnvironmentVariableProvider — это тип ITestHostControllersExtension, который служит основой для всех расширений контроллера узла тестирования. Как и все остальные точки расширения, он также наследует от IExtension. Таким образом, как и любое другое расширение, можно включить или отключить его с помощью API IExtension.IsEnabledAsync.
Рассмотрим сведения об этом API:
UpdateAsync. Этот API обновления предоставляет экземпляр объекта IEnvironmentVariables, из которого можно вызвать методы SetVariable или RemoveVariable. При использовании SetVariableнеобходимо передать объект типа EnvironmentVariable, который требует следующих спецификаций:
-
Variable: имя переменной среды. -
Value: значение переменной среды. -
IsSecret. Это указывает, содержит ли переменная среды конфиденциальную информацию, которая не должна быть зарегистрирована или доступна черезTryGetVariable. -
IsLocked. Это определяет, могут ли другие расширенияITestHostEnvironmentVariableProviderизменять это значение.
ValidateTestHostEnvironmentVariablesAsync: этот метод вызывается после вызова всех методов UpdateAsync зарегистрированных экземпляров ITestHostEnvironmentVariableProvider. Он позволяет проверить правильную настройку переменных среды. Он принимает объект, реализующий IReadOnlyEnvironmentVariables, который предоставляет метод TryGetVariable для получения определенных сведений об переменной среды с типом объекта OwnedEnvironmentVariable. После проверки вы возвращаете ValidationResult со всеми причинами сбоя.
Заметка
Платформа тестирования по умолчанию реализует и регистрирует SystemEnvironmentVariableProvider. Этот поставщик загружает все текущие переменные среды. В качестве первого зарегистрированного поставщика он выполняется первым, предоставляя доступ к переменным среды по умолчанию для всех остальных расширений пользователей ITestHostEnvironmentVariableProvider.
Если расширение требует интенсивной инициализации и необходимо использовать шаблон async/await, можно обратиться к Async extension initialization and cleanup. Если вам нужно обмениваться состоянием между точками расширения, вы можете обратиться к разделу CompositeExtensionFactory<T>.
Расширения ITestHostProcessLifetimeHandler
ITestHostProcessLifetimeHandler — это расширение внеопроцессное, которое позволяет наблюдать за процессом тестового узла извне. Это гарантирует, что расширение не влияет на потенциальные сбои или зависания, которые могут быть вызваны тестируемым кодом. Использование этой точки расширения предложит платформе тестирования инициировать новый хост, как описано в разделе архитектуры.
Чтобы зарегистрировать пользовательскую настройку ITestHostProcessLifetimeHandler, используйте следующий API:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
builder.TestHostControllers.AddProcessLifetimeHandler(
static serviceProvider => new CustomMonitorTestHost());
Фабрика использует IServiceProvider для получения доступа к набору служб, предлагаемых платформой тестирования.
Важный
Последовательность регистрации является важной, так как API вызываются в том порядке, в который они были зарегистрированы.
Интерфейс ITestHostProcessLifetimeHandler включает следующие методы:
public interface ITestHostProcessLifetimeHandler : ITestHostControllersExtension
{
Task BeforeTestHostProcessStartAsync(CancellationToken cancellationToken);
Task OnTestHostProcessStartedAsync(
ITestHostProcessInformation testHostProcessInformation,
CancellationToken cancellation);
Task OnTestHostProcessExitedAsync(
ITestHostProcessInformation testHostProcessInformation,
CancellationToken cancellation);
}
public interface ITestHostProcessInformation
{
int PID { get; }
int ExitCode { get; }
bool HasExitedGracefully { get; }
}
ITestHostProcessLifetimeHandler — это тип ITestHostControllersExtension, который служит основой для всех расширений контроллера узла тестирования. Как и все остальные точки расширения, он также наследует от IExtension. Таким образом, как и любое другое расширение, можно включить или отключить его с помощью API IExtension.IsEnabledAsync.
Рассмотрим следующие сведения для этого API:
BeforeTestHostProcessStartAsync: этот метод вызывается перед тем, как платформа тестирования инициирует тестовые узлы.
OnTestHostProcessStartedAsync: этот метод вызывается сразу после запуска тестового узла. Этот метод предлагает объект, реализующий интерфейс ITestHostProcessInformation, который предоставляет ключевые сведения о результатах процесса узла тестирования.
Важный
Вызов этого метода не останавливает выполнение тестового узла. Если необходимо приостановить его, следует зарегистрировать внутрипроцессное расширение , например ITestHostApplicationLifetime, и синхронизировать его с внепроцессным расширением .
OnTestHostProcessExitedAsync: этот метод вызывается при завершении выполнения набора тестов. Этот метод предоставляет объект, который соответствует интерфейсу ITestHostProcessInformation, который передает важные сведения о результатах процесса тестового узла.
Интерфейс ITestHostProcessInformation предоставляет следующие сведения:
-
PID: идентификатор процесса тестового узла. -
ExitCode: код выхода процесса. Это значение доступно только в методеOnTestHostProcessExitedAsync. Попытка доступа к нему в методеOnTestHostProcessStartedAsyncприведет к исключению. -
HasExitedGracefully: логическое значение, указывающее, произошел ли сбой узла тестирования. Если значение true, это означает, что хост теста не завершился корректно.
Автоматически зарегистрируйте ваше расширение с помощью TestingPlatformBuilderHook
В каждом предыдущем разделе расширения показан вызов регистрации вручную (например, builder.TestHost.AddDataConsumer(...)). Просить пользователей редактировать свой метод Main — это плохой первый опыт работы. Пакет Microsoft.Testing.Platform.MSBuild решает эту проблему путем создания метода SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args), который выполняется из автоматической точки входа. Чтобы подключить расширение к созданному методу, отправьте два артефакта в пакет NuGet:
- Общедоступный статический
TestingPlatformBuilderHookкласс с методомAddExtensions, который регистрирует расширение. - Файл props для MSBuild, объявляющий элемент
<TestingPlatformBuilderHook>, указывающий на этот класс.
Когда пользователь устанавливает ваш пакет, интеграция MSBuild обнаруживает этот элемент и генерирует вызов вашего хука, а ваше расширение регистрируется без каких-либо изменений кода со стороны пользователя.
Заметка
Автоматическая регистрация работает только в том случае, если в проекте потребителя есть Microsoft.Testing.Platform.MSBuild (он добавляется как транзитивная зависимость средствами запуска MSTest, NUnit и xUnit) и если потребитель не отказался от этого, задав <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>. Пользователи, которые отключают автоматически созданную точку входа, по-прежнему должны вызывать API ручной регистрации из метода Main.
Создайте класс хука
Добавьте в сборку расширения public static class TestingPlatformBuilderHook метод AddExtensions(ITestApplicationBuilder, string[]), который выполняет ту же регистрацию, которую в противном случае пользователям пришлось бы вызывать вручную:
using Microsoft.Testing.Platform.Builder;
namespace Contoso.MyExtension;
public static class TestingPlatformBuilderHook
{
public static void AddExtensions(ITestApplicationBuilder testApplicationBuilder, string[] arguments)
=> testApplicationBuilder.AddMyExtension();
}
Имя класса не должно быть TestingPlatformBuilderHook — элемент MSBuild указывает на него по имени полного типа, но использование этого имени сохраняет код в соответствии с встроенными расширениями, такими как Microsoft.Testing.Extensions.Retry и Microsoft.Testing.Extensions.HotReload.
Метод должен:
- Будьте
public static. - Укажите первый параметр типа
Microsoft.Testing.Platform.Builder.ITestApplicationBuilder. - Иметь второй параметр типа
string[](аргументы командной строки, переданные тестовому хосту). Вы можете не обращать на это внимания, если вашему расширению это не нужно. - Вернуться
void.
Объявите элемент MSBuild
Поместите файл props в каталог buildMultiTargeting/<PackageId>.props внутри вашего пакета NuGet. Объявите элемент <TestingPlatformBuilderHook>, который указывает задаче MSBuild на ваш класс-перехватчик:
<Project>
<ItemGroup>
<TestingPlatformBuilderHook Include="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
<DisplayName>Contoso.MyExtension</DisplayName>
<TypeFullName>Contoso.MyExtension.TestingPlatformBuilderHook</TypeFullName>
</TestingPlatformBuilderHook>
</ItemGroup>
</Project>
Метаданные приведены следующим образом:
-
Include: GUID, который однозначно идентифицирует ваш крючок. См.IncludeGUID — это случайный идентификатор. -
DisplayName: понятное имя, отображаемое в диагностических сообщениях MSBuild при создании точки входа. Используйте имя пакета или расширения. -
TypeFullName: полное имя классаTestingPlatformBuilderHook, который вы создали ранее. Задача MSBuild использует эту задачу для отправкиglobal::Contoso.MyExtension.TestingPlatformBuilderHook.AddExtensions(builder, args);в созданную точку входа.
Include GUID — это случайный идентификатор
GUID в атрибуте Includeне совпадает с IExtension.Uid вашего расширения. Это идентификатор регистрации, который задача MSBuild использует для устранения дубликатов хуков среди ссылок на NuGet и (в нескольких общеизвестных случаях) для их упорядочивания.
Когда вы создаёте новое расширение, сгенерируйте совершенно новый GUID и жёстко закодируйте его в вашем файле props. Несколько способов создать это:
- Powershell:
[guid]::NewGuid() - Visual Studio: Средства>Создать GUID
-
uuidgenв Linux и macOS
Важный
Никогда не копируйте GUID из файла props другого расширения (независимо от того, поставляется ли Microsoft или сторонним поставщиком). Два расширения, которые имеют одно и то же Include значение, обрабатываются как дубликаты: вызывается только один перехватчик, поэтому расширение автоматически не регистрируется.
Заметка
После отправки GUID обработайте его как постоянный. Само по себе изменение этого в более поздней версии безвредно, но повторное использование старого значения для другого хука в будущей версии пакета может запутать пользователей, у которых во время обновления в графе зависимостей присутствуют обе версии.
Убедитесь, что крючок подключен
После установки пакета в тестовом проекте, использующего Microsoft.Testing.Platform.MSBuild, создайте проект и проверьте созданный файл SelfRegisteredExtensions.g.cs в obj/<Configuration>/<TargetFramework>/. Вы должны увидеть вызов вашего хука, например:
public static void AddSelfRegisteredExtensions(this global::Microsoft.Testing.Platform.Builder.ITestApplicationBuilder builder, string[] args)
{
global::Contoso.MyExtension.TestingPlatformBuilderHook.AddExtensions(builder, args);
}
Если вызов не выполняется, ещё раз проверьте, что файл props помещён в buildMultiTargeting/ (а не в build/) внутри .nupkg, что метаданные DisplayName и TypeFullName присутствуют и что потребитель не установил <GenerateTestingPlatformEntryPoint>false</GenerateTestingPlatformEntryPoint>.
Порядок выполнения расширений
Платформа тестирования состоит из тестового фреймворка и любого количества расширений, которые могут работать внутрипроцессных или внепроцессных. В этом документе описывается последовательность вызовов ко всем потенциальным точкам расширяемости, чтобы обеспечить ясность в отношении времени, когда функция ожидается к вызову.
- ITestHostEnvironmentVariableProvider.UpdateAsync: внепроцессный
- ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync: вне процесса
- ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync : внепроцессный
- Запуск тестового процесса хоста
- ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync: внепроцессное событие может переплетаться с действиями внутрипроцессных расширений в зависимости от условий гонки.
- ITestHostApplicationLifetime.BeforeRunAsync: внутрипроцессный
- ITestSessionLifetimeHandler.OnTestSessionStartingAsync: в процессе
- ITestFramework.CreateTestSessionAsync: в процессе
- ITestFramework.ExecuteRequestAsync: в процессе этот метод можно вызывать один или несколько раз. На этом этапе платформа тестирования передает сведения IMessageBus, которые можно использовать IDataConsumer.
- ITestFramework.CloseTestSessionAsync: в процессе
- ITestSessionLifetimeHandler.OnTestSessionFinishingAsync: в процессе
- ITestHostApplicationLifetime.AfterRunAsync: In-process
- Очистка во время выполнения включает вызов метода dispose и IAsyncCleanableExtension во всех точках расширения.
- ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync: внепроцессный
- Очистка вне процесса включает вызов метода dispose и IAsyncCleanableExtension на всех точках расширения.
Помощники расширений
Платформа тестирования предоставляет набор вспомогательных классов и интерфейсов для упрощения реализации расширений. Эти вспомогательные средства предназначены для упрощения процесса разработки и обеспечения соответствия расширения стандартам платформы.
Асинхронная инициализация и очистка расширений
Создание тестового фреймворка и расширений с помощью фабрик соответствует стандартному механизму создания объектов .NET, который использует синхронные конструкторы. Если расширение требует интенсивной инициализации (например, доступа к файловой системе или сети), оно не может использовать шаблон async/await в конструкторе, так как конструкторы возвращают пустоту, а не Task.
Таким образом, платформа тестирования предоставляет метод инициализации расширения с помощью шаблона async/await с помощью простого интерфейса. Для симметрии он также предлагает асинхронный интерфейс для очистки, который расширения могут легко реализовать.
public interface IAsyncInitializableExtension
{
Task InitializeAsync();
}
public interface IAsyncCleanableExtension
{
Task CleanupAsync();
}
IAsyncInitializableExtension.InitializeAsync: этот метод должен вызываться после фабрики создания.
IAsyncCleanableExtension.CleanupAsync: этот метод гарантированно вызывается по крайней мере один раз во время завершения сеанса тестирования до DisposeAsync по умолчанию или Dispose.
Важный
Аналогично стандартному методу Dispose, CleanupAsync может вызываться несколько раз. Если метод CleanupAsync объекта вызывается несколько раз, объект должен игнорировать все вызовы после первого. Объект не должен вызывать исключение, если его метод CleanupAsync вызывается несколько раз.
Заметка
По умолчанию платформа тестирования будет вызывать DisposeAsync, если она доступна, или Dispose, если она реализована. Важно отметить, что платформа тестирования не будет вызывать оба метода удаления, но будет определять приоритет асинхронного, если он реализован.
КомпозитнаяФабрикаРасширений<T>
Как описано в разделе расширений, платформа тестирования позволяет реализовать интерфейсы для интеграции пользовательских расширений как в рамках процесса, так и за его пределами.
Каждый интерфейс обращается к определенной функции и в соответствии с .NET проектированием реализует этот интерфейс в определенном объекте. Вы можете зарегистрировать расширение с помощью конкретного API регистрации AddXXX из объекта TestHost или TestHostController из ITestApplicationBuilder, как описано в соответствующих разделах.
Однако, если вам нужно поделиться состоянием между двумя расширениями, факт, что можно реализовать и зарегистрировать разные объекты, которые используют различные интерфейсы, делает эту задачу сложной. Без какой-либо помощи вам потребуется способ передать одно расширение другому, чтобы поделиться информацией, что усложняет проектирование.
Таким образом, платформа тестирования предоставляет изощренный метод для реализации нескольких точек расширения с использованием одного типа, что делает общий доступ к данным простой задачей. Все, что необходимо сделать, — использовать CompositeExtensionFactory<T>, который затем можно зарегистрировать с помощью того же API, что и для реализации одного интерфейса.
Например, рассмотрим тип, реализующий как ITestSessionLifetimeHandler, так и IDataConsumer. Это распространенный сценарий, так как часто требуется собирать сведения из платформы тестирования , а затем при завершении сеанса тестирования вы будете отправлять артефакт с помощью IMessageBus в ITestSessionLifetimeHandler.OnTestSessionFinishingAsync.
Что вам следует сделать — это нормально реализовать интерфейсы:
internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ...
{
...
}
После создания CompositeExtensionFactory<CustomExtension> для вашего типа, его можно зарегистрировать с помощью API IDataConsumer и ITestSessionLifetimeHandler, которые поддерживают перегрузку для CompositeExtensionFactory<T>:
var builder = await TestApplication.CreateBuilderAsync(args);
// ...
var factory = new CompositeExtensionFactory<CustomExtension>(serviceProvider => new CustomExtension());
builder.TestHost.AddTestSessionLifetimeHandle(factory);
builder.TestHost.AddDataConsumer(factory);
Конструктор фабрики использует IServiceProvider для доступа к службам, предоставляемым платформой тестирования.
Платформа тестирования будет отвечать за управление жизненным циклом составного расширения.
Важно отметить, что из-за поддержки платформы тестирования как для внутрипроцессных, так и для внепроцессных расширений нельзя произвольно объединить любую точку расширения. Создание и использование расширений зависят от типа узла, то есть можно группировать только расширения, работающие внутри процесса, (TestHost) и расширения, работающие вне процесса, (TestHostController).
Возможны следующие сочетания:
- Для
ITestApplicationBuilder.TestHostможно объединитьIDataConsumerиITestSessionLifetimeHandler. - Для
ITestApplicationBuilder.TestHostControllersможно объединитьITestHostEnvironmentVariableProviderиITestHostProcessLifetimeHandler.
Заметка
IDataConsumer — это внутрипроцессное расширение, поэтому пользовательские потребители регистрируются только через builder.TestHost (в том числе через CompositeExtensionFactory<T>). Не существует общедоступного API для регистрации IDataConsumer в builder.TestHostControllers.