From 4e47ea3ea32520e8899f267cdec3e3654239d720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 11 Jul 2024 22:53:14 +0200 Subject: [PATCH 01/19] Add testing platform architecture --- ...ting-platform-architecture-capabilities.md | 126 ++ ...esting-platform-architecture-extensions.md | 1164 +++++++++++++++++ ...-testing-platform-architecture-services.md | 357 +++++ .../unit-testing-platform-architecture.md | 154 +++ .../unit-testing-platform-exit-codes.md | 2 +- .../testing/unit-testing-platform-intro.md | 24 + docs/navigate/devops-testing/toc.yml | 12 +- 7 files changed, 1836 insertions(+), 3 deletions(-) create mode 100644 docs/core/testing/unit-testing-platform-architecture-capabilities.md create mode 100644 docs/core/testing/unit-testing-platform-architecture-extensions.md create mode 100644 docs/core/testing/unit-testing-platform-architecture-services.md create mode 100644 docs/core/testing/unit-testing-platform-architecture.md diff --git a/docs/core/testing/unit-testing-platform-architecture-capabilities.md b/docs/core/testing/unit-testing-platform-architecture-capabilities.md new file mode 100644 index 0000000000000..4ac739504caf6 --- /dev/null +++ b/docs/core/testing/unit-testing-platform-architecture-capabilities.md @@ -0,0 +1,126 @@ +--- +title: Microsoft.Testing.Platform architecture overview +description: Learn about how to extend Microsoft.Testing.Platform. +author: MarcoRossignoli +ms.author: mrossignoli +ms.date: 07/11/2024 +--- + +# Microsoft.Testing.Platform capabilities + +In the context of the testing platform, a *capability* refers to the *potential to perform a specific action or provide specific information*. It is a means for the testing framework and extensions to *declare* their *ability* to *operate* in a certain manner or provide specific information to the *requesters*. + +The *requesters* can be any component involved in a test session, such as the platform itself, an extension, or the testing framework itself. + +The primary objective of the capability system is to facilitate effective communication among the components involved in a test session, enabling them to exchange information and meet their respective needs accurately. + +## Guided example + +Let's consider a hypothetical example to demonstrate the necessity of a capability system. **Please note that this example is purely for illustrative purposes and is not currently implemented within the testing platform or any testing framework.** + +Imagine a situation where we have an extension that requires the testing framework to execute no more than one test at a time. Furthermore, after each test, the extension needs to know the CPU usage for that specific test. + +To accommodate the above scenario, we need to inquire from the testing framework if: + +1. It has the capability to execute only one test at a time. +2. It can provide information regarding the amount of CPU consumed by each test. + +How can the extension determine if the testing framework has the ability to operate in this mode and provide CPU usage information for a test session? +In the testing platform, this capability is represented by an implementation of an interface that inherits from a base one named `Microsoft.Testing.Platform.Capabilities.ICapability`: + +```cs +// Base capabilities contracts + +public interface ICapability +{ +} + +public interface ICapabilities + where TCapability : ICapability +{ + IReadOnlyCollection Capabilities { get; } +} + +// Specific testing framework capabilities + +public interface ITestFrameworkCapabilities : ICapabilities +{ +} + +public interface ITestFrameworkCapability : ICapability +{ +} +``` + +As you can see, the interface `ICapability` is *empty* because it can represent *any capability*, and the actual implementation will be context-dependent. You'll also observe the `ITestFrameworkCapability`, which inherits from `ICapability` to classify the capability. The capability system's generic nature allows for convenient grouping by context. The `ITestFrameworkCapability` groups all the capabilities implemented by the [testing framework](./unit-testing-platform-architecture-extensions.md#creating-a-testing-framework). The `ICapabilities` interface reveals the *set* of all capabilities implemented by an extension. Similarly, for the base one, we have a context-specific testing framework called `ITestFrameworkCapabilities`. The `ITestFrameworkCapabilities` is provided to the platform during the [testing framework registration](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework) process. + +Now, let's attempt to create our capability to address the aforementioned scenario. We could define it as follows: + +```cs +public interface IDisableParallelismCapability : ITestFrameworkCapability +{ + bool CanDisableParallelism {get;} + bool CanProvidePerTestCPUConsumption {get;} + void Enable(); +} +``` + +If the testing framework implements this interface and we can query it at runtime, we can: + +* Verify if the testing framework has the ability to turn off parallelism `CanDisableParallelism = true` +* Determine if the testing framework can supply CPU usage data `CanProvidePerTestCPUConsumption = true` +* Request the testing adapter to activate this mode by invoking the `Enable()` method before the test session commences + +The hypothetical code fragment inside the extension could be something like: + +```cs +IServiceProvider serviceProvider = ...get service provider... +ITestFrameworkCapabilities testFrameworkCapabilities = serviceProvider.GetRequiredService(); + +// We utilize the handy `GetCapability` API to search for the specific capability we wish to query. +IDisableParallelismCapability? capability = testFrameworkCapabilities.GetCapability(); +if (capability is null) +{ + ...capability not supported... +} +else +{ + capability.Enable(); + if (capability.CanDisableParallelism) + { + ...do something... + } + + if (capability.CanProvidePerTestCPUConsumption) + { + ...do something... + } +} +``` + +The example above illustrates how the capability infrastructure enables a powerful mechanism for communicating abilities between the components involved in a test session. While the sample demonstrates a capability specifically designed for the testing framework, any component can expose and implement extensions that inherit from `ICapability`. + +It's evident that not all details can be communicated through an interface. For example, in the scenario above, what should the extension expected if the `CanProvidePerTestCPUConsumption` is supported? What kind of custom information is expected to be transmitted via the [`IMessageBus`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) by the testing framework? The solution to this is **DOCUMENTATION OF THE CAPABILITY**. It's the responsibility of the capability *owner* to design, ship, and document it as clearly as possible to assist implementors who want to effectively *collaborate* with the extension that requires the specific capability. + +For instance, at the time of writing, the TRX report extension enables the testing framework to implement the necessary capability to accurately generate a TRX report. The extension to register is included in the package , but the capability to implement is found in the *contract only* package . + +In conclusion, let's summarize the primary aspects of the capability system: + +* It is essential for facilitating clear and stable communication between components. +* All capabilities should inherit from `ICapability` or an interface that inherits from it, and are exposed through a collection with the `ICapabilities` interface. +* It aids in the evolution of features without causing breaking changes. If a certain capability is not supported, appropriate action can be taken. +* The responsibility of designing, shipping, and documenting the usage of a capability lies with the *capability owner*. The testing platform can also *own* a capability in the same way as any other extension. + +## Platform capabilities + +## Framework capabilities + +The platform exposes a specialized interface named `ITestFrameworkCapability` that is the base of all capabilities exposed for test frameworks. These capabilities are provided when [registering the test framework to the platform](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework). + +### `IBannerMessageOwnerCapability` + +An optional [test framework capability](#framework-capabilities) that allows the test framework to provide the banner message to the platform. If the message is null or if the capability is not present, the platform will use its default banner message. + +This capability implementation allows to abstract away the various conditions that the test framework may need to consider to decide whether or not the banner message should be displayed. + +The platform exposes the [`IPlatformInformation` service](./unit-testing-platform-architecture-services.md#the-iplatforminformation-service) to provide some information about the platform that could be useful when building your custom banner message. diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md new file mode 100644 index 0000000000000..8e0d58ced8a9f --- /dev/null +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -0,0 +1,1164 @@ +--- +title: Microsoft.Testing.Platform architecture overview +description: Learn about how to extend Microsoft.Testing.Platform. +author: MarcoRossignoli +ms.author: mrossignoli +ms.date: 07/11/2024 +--- + +# Microsoft.Testing.Platform extensions + +The testing platform consists of a [testing framework](#test-framework) and any number of [extensions](#extensiblity-points) that can operate *in-process* or *out-of-process*. + +As outlined in the [architecture](./unit-testing-platform-architecture.md) section, the testing platform is designed to accommodate a variety of scenarios and extensibility points. The primary and essential extension is undoubtedly the [testing framework](#test-framework-extension) that our tests will utilize. Failing to register it will result in a startup error. **The [testing framework](#test-framework-extension) is the sole mandatory extension required to execute a testing session.** + +To support scenarios such as generating test reports, code coverage, retrying failed tests, and other potential features, we need to provide a mechanism that allows other extensions to work in conjunction with the [testing framework](#test-framework-extension) to deliver these features not inherently provided by the [testing framework](#test-framework-extension) itself. + +In essence, the [testing framework](#test-framework-extension) is the primary extension that supplies information about each test that makes up our test suite. It reports whether a specific test has succeeded, failed, skipped, etc., and can provide additional information about each test, such as a human-readable name (referred to as the display name), the source file, and the line where our test begins, among other things. + +The extensibility point allows us to utilize the information provided by the [testing framework](#test-framework-extension) to generate new artifacts or enhance existing ones with additional features. A commonly used extension is the TRX report generator, which subscribes to the [TestNodeUpdateMessage](#the-testnodeupdatemessage-data) and generates an XML report file from it. + +As discussed in the [architecture](./unit-testing-platform-architecture.md), there are certain extension points that *cannot* operate within the same process as the [testing framework](#test-framework-extension). The reasons typically include: + +* The need to modify the *environment variables* of the *test host*. Acting within the test host process itself is *too late*. +* The requirement to *monitor* the process from the outside because the *test host*, where our tests and user code run, might have some *user code bugs* that render the process itself *unstable*, leading to potential *hangs* or *crashes*. In such cases, the extension would crash or hang along with the *test host* process. + +Due to the reasons mentioned above, the extension points are categorized into two types: + +* *In-process extensions*: These extensions operate within the same process as the [testing framework](#test-framework-extension). + +You can register *in-process extensions* via the `ITestApplicationBuilder.TestHost` property: + +```cs +... +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +testApplicationBuilder.TestHost.AddXXX(...); +... +``` + +* *Out-of-process extensions*: These extensions function in a separate process, allowing them to monitor the test host without being influenced by the test host itself. + +You can register *out-of-process extensions* via the `ITestApplicationBuilder.TestHostControllers`. + +```cs +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +testApplicationBuilder.TestHostControllers.AddXXX(...); +``` + +Lastly, some extensions are designed to function in both scenarios. These common extensions behave identically in both *hosts*. You can register these extensions either through the *TestHost* and *TestHostController* interfaces or directly at the `ITestApplicationBuilder` level. An example of such an extension is the [ICommandLineOptionsProvider](#the-icommandlineoptionsprovider-extensions). + +## The `IExtension` interface + +The `IExtension` interface serves as the foundational interface for all extensibility points within the testing platform. It is primarily used to obtain descriptive information about the extension and, most importantly, to enable or disable the extension itself. + +Let's delve into the specifics: + +```cs +public interface IExtension +{ + string Uid { get; } + string Version { get; } + string DisplayName { get; } + string Description { get; + Task IsEnabledAsync(); +} +``` + +`Uid`: This is the unique identifier for the extension. It's crucial to choose a unique value for this string to avoid conflicts with other extensions. + +`Version`: This represents the version of the interface. It MUST use [**semantic versioning**](https://semver.org/). + +`DisplayName`: This is a user-friendly name that will appear in logs and when you request information using the `--info` command line option. + +`Description`: The description of the extension, will appear when you request information using the `--info` command line option. + +`IsEnabledAsync()`: This method is invoked by the testing platform when the extension is being instantiated. If the method returns false, the extension will be excluded. +This method typically makes decisions based on the [configuration file](./unit-testing-platform-architecture-services.md#the-iconfiguration-service) file or some [custom command line options](./unit-testing-platform-architecture-services.md#the-icommandlineoptions-service). Users often specify `--customExtensionOption` in the command line to opt into the extension itself. + +## Test Framework extension + +### Registering a testing framework + +This section explains how to register the test framework to the testing platform. +You can register only one testing framework per test application builder using the api `TestApplication.RegisterTestFramework` as shown [here](./unit-testing-platform-architecture.md) + +The API's signature is as follows: + +```cs +ITestApplicationBuilder RegisterTestFramework( + Func capabilitiesFactory, + Func adapterFactory); +``` + +The `RegisterTestFramework` API expects two factories: + +1. `Func`: This is a lambda function that accepts an object implementing the [`IServiceProvider`](./unit-testing-platform-architecture-services.md) interface and returns an object implementing the [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-./unit-testing-platform-architecture-capabilities.md) interface. The [`IServiceProvider`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) provides access to platform services such as configurations, loggers, command line arguments, etc. + + The [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) interface is used to announce the capabilities supported by the testing framework to the platform and extensions. It allows the platform and extensions to interact correctly by implementing and supporting specific behaviors. For a better understanding of the [concept of capabilities](./unit-testing-platform-architecture-capabilities.md), refer to the respective section. + +1. `Func`: This is a lambda function that takes in an [ITestFrameworkCapabilities](./unit-testing-platform-architecture-capabilities.md) object, which is the instance returned by the `Func`, and an [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to provide access to platform services once more. The expected return object is one that implements the [ITestFramework](#test-framework-extension) interface. The ITestFramework serves as the execution engine that discovers and runs tests, and communicates the results back to the testing platform. + +The need for the platform to separate the creation of the [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) and the creation of the [ITestFramework](#test-framework-extension) is an optimization to avoid creating the test framework if the supported capabilities are not sufficient to execute the current testing session. + +Below a sample of a test framework registration that returns empty capabilities. + +User code: + +```cs +internal class TestingFrameworkCapabilities : ITestFrameworkCapabilities +{ + public IReadOnlyCollection Capabilities => []; +} + +internal class TestingFramework : ITestFramework +{ + public TestingFramework(ITestFrameworkCapabilities capabilities, IServiceProvider serviceProvider) + { + ... + } + ... +} + +public static class TestingFrameworkExtensions +{ + public static void AddTestingFramework(this ITestApplicationBuilder builder) + { + builder.RegisterTestFramework( + _ => new TestingFrameworkCapabilities(), + (capabilities, serviceProvider) => new TestingFramework(capabilities, serviceProvider)); + } +} + +... +``` + +Entry point with the registration: + +```cs +var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +// Register the testing framework +testApplicationBuilder.AddTestingFramework(); +using var testApplication = await testApplicationBuilder.BuildAsync(); +return await testApplication.RunAsync(); +``` + +> [!NOTE] +> Returning empty [ITestFrameworkCapabilities](./unit-testing-platform-architecture-capabilities.md) should not prevent the execution of the test session. All test frameworks should be capable of discovering and running tests. The impact should be limited to extensions that may opt out if the test framework lacks a certain feature. + +### Creating a testing framework + +The `Microsoft.Testing.Platform.Extensions.TestFramework.ITestFramework` is implemented by extensions that provide a test framework: + +```cs +public interface ITestFramework : IExtension +{ + Task CreateTestSessionAsync(CreateTestSessionContext context); + Task ExecuteRequestAsync(ExecuteRequestContext context); + Task CloseTestSessionAsync(CloseTestSessionContext context); +} +``` + +The `ITestFramework` interface inherits from the [IExtension](#the-iextension-interface) interface, which is an interface that all extension points inherit from. `IExtension` is used to retrieve the name and description of the extension. The `IExtension` also provides a way to dynamically enable or disable the extension in setup, through `Task IsEnabledAsync()`, please make sure that you return `true` from this method if you have no special needs. + +#### CreateTestSessionAsync + +The `CreateTestSessionAsync` is called at the start of the test session and is used to initialize the test framework. As we can see the api accepts a `CloseTestSessionContext` object and returns a `CloseTestSessionResult`. + +```cs +public sealed class CreateTestSessionContext : TestSessionContext +{ + public SessionUid SessionUid { get; } + public ClientInfo Client { get; } + public CancellationToken CancellationToken { get; } +} + +public readonly struct SessionUid +{ + public string Value { get; } +} + +public sealed class ClientInfo +{ + public string Id { get; } + public string Version { get; } +} +``` + +The `SessionUid` serves as the unique identifier for the current test session, providing a logical connection to the session's results. +The `ClientInfo` provides details about the entity invoking the test framework. This information can be utilized by the test framework to modify its behavior. For example, as of the time this document was written, a console execution would report a client name such as "testingplatform-console". +The `CancellationToken` is used to halt the execution of `CreateTestSessionAsync`. + +The return object is a `CloseTestSessionResult`: + +```cs +public sealed class CreateTestSessionResult +{ + public string? WarningMessage { get; set; } + public string? ErrorMessage { get; set; } + public bool IsSuccess { get; set; } +} +``` + +The `IsSuccess` can be used to indicate whether the session creation was successful. If it returns false, the test execution will be halted. + +#### CloseTestSessionAsync + +The `CloseTestSessionAsync` mirrors the `CreateTestSessionAsync` in functionality, with the only difference being the object names. For more details, refer to the `CreateTestSessionAsync` section. + +#### ExecuteRequestAsync + +The `ExecuteRequestAsync` accepts an object of type `ExecuteRequestContext`. This object, as suggested by its name, holds the specifics about the action that the test framework is expected to perform. +The `ExecuteRequestContext` definition is: + +```cs +public sealed class ExecuteRequestContext +{ + public IRequest Request { get; } + public IMessageBus MessageBus { get; } + public CancellationToken CancellationToken { get; } + public void Complete(); +} +``` + +`IRequest`: This is the base interface for any type of request. We should think about the test framework as an **in-process stateful server** where the lifecycle is: + +```mermaid +sequenceDiagram + Testing platform->>ITestFramework: adapterFactory() from 'RegisterTestFramework' + ITestFramework-->>Testing platform: + Testing platform->>ITestFramework: CreateTestSessionAsync(CreateTestSessionContext) + ITestFramework-->>Testing platform: CreateTestSessionResult + Testing platform->>ITestFramework: ExecuteRequestAsync(ExecuteRequestContext_1) + Testing platform->>ITestFramework: ExecuteRequestAsync(ExecuteRequestContext_2) + ITestFramework->>IMessageBus: PublishAsync() for ExecuteRequestContext_1 + Testing platform->>ITestFramework: ExecuteRequestAsync(ExecuteRequestContext_3) + ITestFramework->>IMessageBus: PublishAsync() for ExecuteRequestContext_3 + ITestFramework->>IMessageBus: PublishAsync() for ExecuteRequestContext_2 + ITestFramework->>IMessageBus: PublishAsync() for ExecuteRequestContext_2 + ITestFramework->>IMessageBus: PublishAsync() for ... + ITestFramework->>ExecuteRequestContext_1: Complete() + ITestFramework->>ExecuteRequestContext_3: Complete() + ITestFramework->>ExecuteRequestContext_2: Complete() + Testing platform->>ITestFramework: CloseTestSessionAsync(CloseTestSessionContext) + ITestFramework-->>Testing platform: CloseTestSessionResult +``` + +The diagram above illustrates that the testing platform issues 3 requests after creating the test framework instance. The test framework processes these requests and utilizes the `IMessageBus` service, which is included in the request itself, to deliver the result for each specific request. Once a particular request has been handled, the test framework invokes the `Complete()` method on it, indicating to the testing platform that the request has been fulfilled. +The testing platform monitors all dispatched requests. Once all requests have been fulfilled, it invokes `CloseTestSessionAsync` and disposes of the instance (if `IDisposable/IAsyncDisposable` is implemented). +It's evident that the requests and their completions can overlap, enabling concurrent and asynchronous execution of requests. +> [!NOTE] +> Currently, the testing platform does not send overlapping requests and waits for the completion of a request >> before sending the next one. However, this behavior may change in the future. The support for concurrent requests will be determined through the [capabilities](./unit-testing-platform-architecture-capabilities.md) system. + +The `IRequest` implementation specifies the precise request that needs to be fulfilled. The test framework identifies the type of request and handles it accordingly. If the request type is unrecognized, an exception should be raised. + +You can find details about the available requests in the [IRequest](#handling-requests) section. + +`IMessageBus`: This service, linked with the request, allows the test framework to *asynchronously* to publish information about the ongoing request to the testing platform. +The message bus serves as the central hub for the platform, facilitating asynchronous communication among all platform components and extensions. +For a comprehensive list of information that can be published to the testing platform, refer to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) section. + +`CancellationToken`: This token is utilized to interrupt the processing of a particular request. + +`Complete()`: As depicted in the previous sequence, the `Complete` method notifies the platform that the request has been successfully processed and all relevant information has been transmitted to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service). +> [!WARNING] +> Neglecting to invoke `Complete()` on the request will result in the test application becoming unresponsive. + +To customize your test framework according to your requirements or those of your users, you can use a personalized section inside the [configuration](./unit-testing-platform-architecture-services.md#the-iconfiguration-service) file or with custom [command line options](#the-icommandlineoptionsprovider-extensions). + +### Handling requests + +The subsequent section provides a detailed description of the various requests that a test framework may receive and process. + +Before proceeding to the next section, it's crucial to thoroughly comprehend the concept of the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service), which is the essential service for conveying test execution information to the testing platform. + +#### TestSessionContext + +The `TestSessionContext` is a shared property across all requests, providing information about the ongoing test session: + +```cs +public class TestSessionContext +{ + public SessionUid SessionUid { get; } + public ClientInfo Client { get; } +} + +public readonly struct SessionUid(string value) +{ + public string Value { get; } +} + +public sealed class ClientInfo +{ + public string Id { get; } + public string Version { get; } +} +``` + +The `TestSessionContext` consists of the `SessionUid`, a unique identifier for the ongoing test session that aids in logging and correlating test session data. It also includes the `ClientInfo` type, which provides details about the *initiator* of the test session. The test framework may choose different routes or publish varying information based on the identity of the test session's *initiator*. + +#### DiscoverTestExecutionRequest + +```cs +public class DiscoverTestExecutionRequest +{ + // Detailed in the custom section below + public TestSessionContext Session { get; } + + // This is experimental and intended for future use, please disregard for now. + public ITestExecutionFilter Filter { get; } +} +``` + +The `DiscoverTestExecutionRequest` instructs the test framework **to discover** the tests and communicate this information thought to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service). + +As outlined in the previous section, the property for a discovered test is `DiscoveredTestNodeStateProperty`. Here is a generic code snippet for reference: + +```cs +... +var testNode = new TestNode() +{ + Uid = GenerateUniqueStableId(), + DisplayName = GetDisplayName(), + Properties = new PropertyBag(DiscoveredTestNodeStateProperty.CachedInstance), +}; + +await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(discoverTestExecutionRequest.Session.SessionUid, testNode)); +... +``` + +#### RunTestExecutionRequest + +```cs +public class RunTestExecutionRequest +{ + // Detailed in the custom section below + public TestSessionContext Session { get; } + + // This is experimental and intended for future use, please disregard for now. + public ITestExecutionFilter Filter { get; } +} +``` + +The `RunTestExecutionRequest` instructs the test framework **to execute** the tests and communicate this information thought to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service). + +Here is a generic code snippet for reference: + +```cs +... +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)); +``` + +### The `TestNodeUpdateMessage` data + +As mentioned in the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) section, before utilizing the message bus, you must specify the type of data you intend to supply. The testing platform has defined a well-known type, `TestNodeUpdateMessage`, to represent the concept of a *test update information*. +This part of the document will explain how to utilize this payload data. Let's examine the surface: + +```cs +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 properties); + public int Count { get; } + public void Add(IProperty property); + public bool Any(); + public TProperty? SingleOrDefault(); + public TProperty Single(); + public TProperty[] OfType(); + public IEnumerable AsEnumerable(); + public IEnumerator GetEnumerator(); + ... +} + +public interface IProperty +{ +} +``` + +* `TestNodeUpdateMessage`: The `TestNodeUpdateMessage` consists of two properties: a `TestNode`, which we will discuss in this section, and a `ParentTestNodeUid`. The `ParentTestNodeUid` indicates that a test may have a parent test, introducing the concept of a **test tree** where `TestNode`s can be arranged in relation to each other. This structure allows for future enhancements and features based on the *tree* relationship between the nodes. If your test framework doesn't require a test tree structure, you can opt not to use it and simply set it to null, resulting in a straightforward flat list of `TestNode`s. + +* `TestNode`: The `TestNode` is composed of three properties, one of which is the `Uid` of type `TestNodeUid`. This `Uid` serves as the **UNIQUE STABLE ID** for the node. The term **UNIQUE STABLE ID** implies that the same `TestNode` should maintain an **IDENTICAL** `Uid` across different runs and operating systems. The `TestNodeUid` is an **arbitrary opaque string** that the testing platform accepts as is. + +> [!IMPORTANT] +> The stability and uniqueness of the ID are crucial in the testing domain. They enable the precise targeting of a single test for execution and allow the ID to serve as a persistent identifier for a test, facilitating powerful extensions and features. + +The second property is `DisplayName`, which is the human-friendly name for the test. For example, this name is displayed when you execute the `--list-tests` command line. + +The third attribute is `Properties`, which is a `PropertyBag` type. As demonstrated in the code, this is a specialized property bag that holds generic properties about the `TestNodeUpdateMessage`. This implies that you can append any property to the node that implements the placeholder interface `IProperty`. + +***The testing platform identifies specific properties added to a `TestNode.Properties` to determine whether a test has passed, failed, or been skipped.*** + +You can find the current list of available properties with the relative description in the section [TestNodeUpdateMessage.TestNode](#the-testnodeupdatemessage-data) + +The `PropertyBag` type is typically accessible in every `IData` and is utilized to store miscellaneous properties that can be queried by the platform and extensions. This mechanism allows us to enhance the platform with new information without introducing breaking changes. If a component recognizes the property, it can query it; otherwise, it will disregard it. + +Finally this section makes clear that you test framework implementation needs to implement the `IDataProducer` that produces `TestNodeUpdateMessage`s like in the sample below: + +```cs +internal sealed class TestingFramework : ITestFramework, IDataProducer +{ + ... + public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage) }; + ... +} +``` + +If your test adapter requires the publication of *files* during execution, you can find the recognized properties in this source file: . As you can see, you can provide file assets in a general manner or associate them with a specific `TestNode`. Remember, if you intend to push a `SessionFileArtifact`, you must declare it to the platform in advance, as shown below: + +```cs +internal sealed class TestingFramework : ITestFramework, IDataProducer +{ + ... + public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage), typeof(SessionFileArtifact) }; + ... +} +``` + +#### Well-known properties + +As detailed in the [requests section](#handling-requests), the testing platform identifies specific properties added to the `TestNodeUpdateMessage` to determine the status of a `TestNode` (e.g., successful, failed, skipped, etc.). This allows the runtime to accurately display a list of failed tests with their corresponding information in the console, and to set the appropriate exit code for the test process. + +In this segment, we'll elucidate the various well-known `IProperty` options and their respective implications. + +If you're looking for a comprehensive list of well-known properties, you can find it [here](https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/Messages/TestNodeProperties.cs). If you notice that a property description is missing, please don't hesitate to file an issue. + +We can divide the properties in: + +1. [*Generic information*](#generic-information): Properties that can be included in any kind of request. +1. [*Discovery information*](#discovery-information): Properties that are supplied during a `DiscoverTestExecutionRequest` discovery request. +1. [*Execution information*](#execution-information): Properties that are supplied during a test execution request `RunTestExecutionRequest`. + +Certain properties are **required**, while others are optional. The mandatory properties are required to provide basic testing functionality, such as reporting failed tests and indicating whether the entire test session was successful or not. + +Optional properties, on the other hand, enhance the testing experience by providing additional information. They are particularly useful in IDE scenarios (like VS, VSCode, etc.), console runs, or when supporting specific extensions that require more detailed information to function correctly. However, these optional properties do not affect the execution of the tests. + +> [!NOTE] +> Extensions are tasked with alerting and managing exceptions when they require specific information to operate correctly. If an extension lacks the necessary information, it should not cause the test execution to fail, but rather, it should simply opt-out. + +##### Generic information + +```cs +public record KeyValuePairStringProperty(string Key, string Value) : IProperty; +``` + +The `KeyValuePairStringProperty` stands for a general key/value pair data. + +```cs +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` is used to pinpoint the location of the test within the source file. This is particularly useful when the initiator is an IDE like Visual Studio or Visual Studio Code. + +```cs +public sealed record TestMethodIdentifierProperty(string AssemblyFullName, string Namespace, string TypeName, string MethodName, string[] ParameterTypeFullNames, string ReturnTypeFullName) +``` + +`TestMethodIdentifierProperty` is a unique identifier for a test method, adhering to the ECMA-335 standard. + +> [!NOTE] +> The data needed to create this property can be conveniently obtained using the .NET reflection feature, using types from the `System.Reflection` namespace. + +```cs +public sealed record TestMetadataProperty(string Key, string Value) +``` + +`TestMetadataProperty` is utilized to convey the characteristics or *traits* of a `TestNode`. + +##### Discovery information + +```cs +public sealed record DiscoveredTestNodeStateProperty(string? Explanation = null) +{ + public static DiscoveredTestNodeStateProperty CachedInstance { get; } +} +``` + +The `DiscoveredTestNodeStateProperty` indicates that this TestNode has been discovered. It is utilized when a `DiscoverTestExecutionRequest` is sent to the test framework. +Take note of the handy cached value offered by the `CachedInstance` property. +This property is **required**. + +##### Execution information + +```cs +public sealed record InProgressTestNodeStateProperty(string? Explanation = null) +{ + public static InProgressTestNodeStateProperty CachedInstance { get; } +} +``` + +The `InProgressTestNodeStateProperty` informs the testing platform that the `TestNode` has been scheduled for execution and is currently in progress. +Take note of the handy cached value offered by the `CachedInstance` property. + +```cs +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) + public TimingProperty(TimingInfo globalTiming, StepTimingInfo[] stepTimings) + public TimingInfo GlobalTiming { get; } + public StepTimingInfo[] StepTimings { get; } +} +``` + +The `TimingProperty` is utilized to relay timing details about the `TestNode` execution. It also allows for the timing of individual execution steps via `StepTimingInfo`. This is particularly useful when your test concept is divided into multiple phases such as initialization, execution, and cleanup. + +***One and only one*** of the following properties is **required** per `TestNode` and communicates the result of the `TestNode` to the testing platform. + +```cs +public sealed record PassedTestNodeStateProperty(string? Explanation = null) +{ + public static PassedTestNodeStateProperty CachedInstance { get; } +} +``` + +`PassedTestNodeStateProperty` informs the testing platform that this `TestNode` is passed. +Take note of the handy cached value offered by the `CachedInstance` property. + +```cs +public sealed record SkippedTestNodeStateProperty(string? Explanation = null) +{ + public static SkippedTestNodeStateProperty CachedInstance { get; } +} +``` + +`SkippedTestNodeStateProperty` informs the testing platform that this `TestNode` was skipped. +Take note of the handy cached value offered by the `CachedInstance` property. + +```cs +public sealed record FailedTestNodeStateProperty +{ + public FailedTestNodeStateProperty(string explanation) + public FailedTestNodeStateProperty(Exception exception, string? explanation = null) + public Exception? Exception { get; } +} +``` + +`FailedTestNodeStateProperty` informs the testing platform that this `TestNode` is failed after an assertion. + +```cs +public sealed record ErrorTestNodeStateProperty +{ + public ErrorTestNodeStateProperty(string explanation) + public ErrorTestNodeStateProperty(Exception exception, string? explanation = null) + public Exception? Exception { get; } +} +``` + +`ErrorTestNodeStateProperty` informs the testing platform that this `TestNode` has failed. This type of failure is different from the `FailedTestNodeStateProperty`, which is used for assertion failures. For example, you can report issues like test initialization errors with `ErrorTestNodeStateProperty`. + +```cs +public sealed record TimeoutTestNodeStateProperty +{ + public TimeoutTestNodeStateProperty(string explanation) + public TimeoutTestNodeStateProperty(Exception exception, string? explanation = null) + public Exception? Exception { get; } + public TimeSpan? Timeout { get; init; } +} +``` + +`TimeoutTestNodeStateProperty` informs the testing platform that this `TestNode` is failed for a timeout reason. You can report the timeout using the `Timeout` property. + +```cs +public sealed record CancelledTestNodeStateProperty +{ + public CancelledTestNodeStateProperty(string explanation) + public CancelledTestNodeStateProperty(Exception exception, string? explanation = null) + public Exception? Exception { get; } +} +``` + +`CancelledTestNodeStateProperty` informs the testing platform that this `TestNode` has failed due to cancellation. + +## Other extensibility points + +### The `ICommandLineOptionsProvider` extensions + +> [!NOTE] +> When extending this API, the custom extension will exists both in and out of the test host process. + +As discussed in the [architecture](./unit-testing-platform-architecture.md) section, the initial step involves creating the `ITestApplicationBuilder` to register the testing framework and extensions with it. + +```cs +... +var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +... +``` + +The `CreateBuilderAsync` method accepts an array of strings (`string[]`) named `args`. These arguments can be used to pass command-line options to all components of the testing platform (including built-in components, testing frameworks, and extensions), allowing for customization of their behavior. + +Typically, the arguments passed are those received in the standard `Main(string[] args)` method. However, if the hosting environment differs, any list of arguments can be supplied. + +Arguments **must be prefixed** with a double dash `--`. For example, `--filter`. + +If a component such as a testing framework or an extension point wishes to offer custom command-line options, it can do so by implementing the `ICommandLineOptionsProvider` interface. This implementation can then be registered with the `ITestApplicationBuilder` via the registration factory of the `CommandLine` property, as shown: + +```cs +... +testApplicationBuilder.CommandLine.AddProvider(() => new CustomCommandLineOptions()); +... +``` + +In the example provided, `CustomCommandLineOptions` is an implementation of the `ICommandLineOptionsProvider` interface, This interface comprises the following members and data types: + +```cs +public interface ICommandLineOptionsProvider : IExtension +{ + IReadOnlyCollection GetCommandLineOptions(); + Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments); + Task ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions); +} + +public sealed class CommandLineOption +{ + public CommandLineOption(string name, string description, ArgumentArity arity, bool isHidden) + 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); +} +``` + +As observed, the `ICommandLineOptionsProvider` extends the [`IExtension`](#the-iextension-interface) interface. Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. + +The order of execution of the `ICommandLineOptionsProvider` is: + +```mermaid +sequenceDiagram + Testing platform->>ICommandLineOptionsProvider: GetCommandLineOptions() + ICommandLineOptionsProvider-->>Testing platform: returns the list of `CommandLineOption` + Testing platform->>ICommandLineOptionsProvider: ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) validate every option argument + ICommandLineOptionsProvider-->>Testing platform: returns a `ValidationResult` indicating success or failure. + Testing platform->>ICommandLineOptionsProvider: ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) Ensure the consistency of all arguments in unison. + ICommandLineOptionsProvider-->>Testing platform: returns a `ValidationResult` indicating success or failure. +``` + +Let's examine the apis and their mean: + +`ICommandLineOptionsProvider.GetCommandLineOptions()`: This method is utilized to retrieve all the options offered by the component. Each `CommandLineOption` requires the following properties to be specified: + +`string name`: This is the option's name, presented without a dash. For example, *filter* would be used as `--filter` by users. + +`string description`: This is a description of the option. It will be displayed when users pass `--help` as an argument to the application builder. + +`ArgumentArity arity`: The arity of an option is the number of values that can be passed if that option or command is specified. Current available arities are: + +* `Zero`: Represents an argument arity of zero. +* `ZeroOrOne`: Represents an argument arity of zero or one. +* `ZeroOrMore`: Represents an argument arity of zero or more. +* `OneOrMore`: Represents an argument arity of one or more. +* `ExactlyOne`: Represents an argument arity of exactly one. + +For examples, refer to the [System.CommandLine arity table](https://learn.microsoft/dotnet/standard/commandline/syntax#argument-arity). + +`bool isHidden`: This property signifies that the option is available for use but will not be displayed in the description when `--help` is invoked. + +`ICommandLineOptionsProvider.ValidateOptionArgumentsAsync`: This method is employed to *validate* the argument provided by the user. + +For instance, if we have a parameter named `--dop` that represents the degree of parallelism for our custom testing framework, a user might input `--dop 0`. In this scenario, the value 0 would be invalid because we anticipate a degree of parallelism of 1 or more. By using `ValidateOptionArgumentsAsync`, we can perform upfront validation and return an error message if necessary. +A possible implementation for the sample above could be: + +```cs + public Task 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`: This method is called as last one and allows to do global coherency check. + +For example, let's say our testing framework has the capability to generate a test result report and save it to a file. This feature is accessed using the `--generatereport` option, and the filename is specified with `--reportfilename myfile.rep`. In this scenario, if a user only provides the `--generatereport` option without specifying a filename, the validation should fail because the report cannot be generated without a filename. +A possible implementation for the sample above could be: + +```cs + public Task 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; + } +``` + +Please note that the `ValidateCommandLineOptionsAsync` method provides the [`ICommandLineOptions`](./unit-testing-platform-architecture-services.md#the-icommandlineoptions-service) service, which is used to fetch the argument information parsed by the platform itself. + +### The `ITestSessionLifetimeHandler` extensions + +The `ITestSessionLifeTimeHandler` is an *in-process* extension that enables the execution of code *before* and *after* the test session. + +To register a custom `ITestSessionLifeTimeHandler`, utilize the following API: + +```cs +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +... +testApplicationBuilder.TestHost.AddTestSessionLifetimeHandle(serviceProvider => new CustomTestSessionLifeTimeHandler()); +... +``` + +The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. + +> [!IMPORTANT] +> The sequence of registration is significant, as the APIs are called in the order they were registered. + +The `ITestSessionLifeTimeHandler` interface includes the following methods: + +```cs +public interface ITestSessionLifetimeHandler : ITestHostExtension +{ + Task OnTestSessionStartingAsync(SessionUid sessionUid, CancellationToken cancellationToken); + Task OnTestSessionFinishingAsync(SessionUid sessionUid, CancellationToken cancellationToken); +} + +public readonly struct SessionUid(string value) +{ + public string Value { get; } +} + +public interface ITestHostExtension : IExtension +{ +} +``` + +The `ITestSessionLifetimeHandler` is a type of `ITestHostExtension`, which serves as a base for all *test host* extensions. Like all other extension points, it also inherits from [IExtension](#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. + +Let's describe the api: + +`OnTestSessionStartingAsync`: This method is invoked prior to the commencement of the test session and receives the `SessionUid` object, which provides an opaque identifier for the current test session. + +`OnTestSessionFinishingAsync`: This method is invoked after the completion of the test session, ensuring that the [testing framework](#test-framework-extension) has finished executing all tests and has reported all relevant data to the platform. Typically, in this method, the extension employs the [`IMessageBus`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to transmit custom assets or data to the shared platform bus. This method can also signal to any custom *out-of-process* extension that the test session has concluded. + +Finally, both APIs take a `CancellationToken` which the extension is expected to honor. + +If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](asyncinitcleanup.md). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](compositeextensionfactory.md) section. + +### The `ITestApplicationLifecycleCallbacks` extensions + +The `ITestApplicationLifecycleCallbacks` is an *in-process* extension that enables the execution of code before everything, it's like to have access to the first line of the hypothetical *main* of the *test host*. + +To register a custom `ITestApplicationLifecycleCallbacks`, utilize the following api: + +```cs +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +... +testApplicationBuilder.TestHost.AddTestApplicationLifecycleCallbacks(serviceProvider + => new CustomTestApplicationLifecycleCallbacks()); +... +``` + +The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. + +> [!IMPORTANT] +> The sequence of registration is significant, as the APIs are called in the order they were registered. + +The `ITestApplicationLifecycleCallbacks` interface includes the following methods: + +```cs +public interface ITestApplicationLifecycleCallbacks : ITestHostExtension +{ + Task BeforeRunAsync(CancellationToken cancellationToken); + Task AfterRunAsync(int exitCode, CancellationToken cancellation); +} + +public interface ITestHostExtension : IExtension +{ +} +``` + +The `ITestApplicationLifecycleCallbacks` is a type of `ITestHostExtension`, which serves as a base for all *test host* extensions. Like all other extension points, it also inherits from [IExtension](#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. + +`BeforeRunAsync`: This method serves as the initial point of contact for the *test host* and is the first opportunity for an *in-process* extension to execute a feature. It's typically used to establish a connection with any corresponding *out-of-process* extensions if a feature is designed to operate across both environments. + +*For example, the built-in hang dump feature is composed of both *in-process* and *out-of-process* extensions, and this method is used to exchange information with the *out-of-process* component of the extension.* + +`AfterRunAsync`: This method is the final call before exiting the [`int ITestApplication.RunAsync()`](./unit-testing-platform-architecture.md) and it provides the [`exit code`](./unit-testing-platform-exit-codes.md). It should be used solely for cleanup tasks and to notify any corresponding *out-of-process* extension that the *test host* is about to terminate. + +Finally, both APIs take a `CancellationToken` which the extension is expected to honor. + +### The `IDataConsumer` extensions + +The `IDataConsumer` is an *in-process* extension capable of subscribing to and receiving `IData` information that is pushed to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) by the [testing framework](#test-framework-extension) and its extensions. + +*This extension point is crucial as it enables developers to gather and process all the information generated during a test session.* + +To register a custom `IDataConsumer`, utilize the following api: + +```cs +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +... +testApplicationBuilder.TestHost.AddDataConsumer(serviceProvider + => new CustomDataConsumer()); +... +``` + +The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. + +> [!IMPORTANT] +> The sequence of registration is significant, as the APIs are called in the order they were registered. + +The `IDataConsumer` interface includes the following methods: + +```cs +public interface IDataConsumer : ITestHostExtension +{ + Type[] DataTypesConsumed { get; } + Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken); +} + +public interface IData +{ + string DisplayName { get; } + string? Description { get; } +} +``` + +The `IDataConsumer` is a type of `ITestHostExtension`, which serves as a base for all *test host* extensions. Like all other extension points, it also inherits from [IExtension](#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. + +`DataTypesConsumed`: This property returns a list of `Type` that this extension plans to consume. It corresponds to `IDataProducer.DataTypesProduced`. Notably, an `IDataConsumer` can subscribe to multiple types originating from different `IDataProducer` instances without any issues. + +`ConsumeAsync`: This method is triggered whenever data of a type to which the current consumer is subscribed is pushed onto the [`IMessageBus`](./unit-testing-platform-architecture-services.md#the-imessagebus-service). It receives the `IDataProducer` to provide details about the data payload's producer, as well as the `IData` payload itself. As you can see, `IData` is a generic placeholder interface that contains general informative data. The ability to push different types of `IData` implies that the consumer needs to *switch* on the type itself to cast it to the correct type and access the specific information. + +A sample implementation of a consumer that wants to elaborate the [`TestNodeUpdateMessage`](#the-testnodeupdatemessage-data) produced by a [testing framework](#test-framework-extension) could be: + +```cs +internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer +{ + public Type[] DataTypesConsumed => new[] { typeof(TestNodeUpdateMessage) }; + ... + public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + { + var testNodeUpdateMessage = (TestNodeUpdateMessage)value; + + TestNodeStateProperty nodeState = testNodeUpdateMessage.TestNode.Properties.Single(); + + switch (nodeState) + { + case InProgressTestNodeStateProperty _: + { + ... + break; + } + case PassedTestNodeStateProperty _: + { + ... + break; + } + case FailedTestNodeStateProperty failedTestNodeStateProperty: + { + ... + break; + } + case SkippedTestNodeStateProperty _: + { + ... + break; + } + ... + } + + return Task.CompletedTask; + } +... +} +``` + +Finally, the api takes a `CancellationToken` which the extension is expected to honor. + +> [!IMPORTANT] +> It's crucial to process the payload directly within the `ConsumeAsync` method. The [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) can manage both synchronous and asynchronous processing, coordinating the execution with the [testing framework](#test-framework-extension). Although the consumption process is entirely asynchronous and doesn't block the [IMessageBus.Push](./unit-testing-platform-architecture-services.md#the-imessagebus-service) at the time of writing, this is an implementation detail that may change in the future due to feature requirements. However, we aim to maintain this interface's simplicity and ensure that this method is always called once, eliminating the need for complex synchronization. Additionally, we automatically manage the scalability of the consumers. + + + +> [!WARNING] +> When using `IDataConsumer` in conjunction with [ITestHostProcessLifetimeHandler](itestsessionlifetimehandler.md) within a [composite extension point](compositeextensionfactory.md), **it's crucial to disregard any data received post the execution of [ITestSessionLifetimeHandler.OnTestSessionFinishingAsync](itestsessionlifetimehandler.md)**. The `OnTestSessionFinishingAsync` is the final opportunity to process accumulated data and transmit new information to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service), hence, any data consumed beyond this point will not be *utilizable* by the extension. + +If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](asyncinitcleanup.md). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](compositeextensionfactory.md) section. + +### The `ITestHostEnvironmentVariableProvider` extensions + +The `ITestHostEnvironmentVariableProvider` is an *out-of-process* extension that enables you to establish custom environment variables for the test host. Utilizing this extension point ensures that the testing platform will initiate a new host with the appropriate environment variables, as detailed in the [architecture](./unit-testing-platform-architecture.md) section. + +To register a custom `ITestHostEnvironmentVariableProvider`, utilize the following API: + +```cs +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +... +testApplicationBuilder.TestHostControllers.AddEnvironmentVariableProvider(serviceProvider => + => new CustomEnvironmentVariableForTestHost()); +... +``` + +The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. + +> [!IMPORTANT] +> The sequence of registration is significant, as the APIs are called in the order they were registered. + +The `ITestHostEnvironmentVariableProvider` interface includes the following methods and types: + +```cs +public interface ITestHostEnvironmentVariableProvider : ITestHostControllersExtension, IExtension +{ + Task UpdateAsync(IEnvironmentVariables environmentVariables); + Task 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; } +} +``` + +The `ITestHostEnvironmentVariableProvider` is a type of `ITestHostControllersExtension`, which serves as a base for all *test host controller* extensions. Like all other extension points, it also inherits from [IExtension](#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. + +Let's describe the api: + +`UpdateAsync`: This update API provides an instance of the `IEnvironmentVariables` object, from which you can call the `SetVariable` or `RemoveVariable` methods. When using `SetVariable`, you must pass an object of type `EnvironmentVariable`, which requires the following specifications: + +* `Variable`: The name of the environment variable. +* `Value`: The value of the environment variable. +* `IsSecret`: This indicates whether the environment variable contains sensitive information that should not be logged or accessible via the `TryGetVariable`. +* `IsLocked`: This determines whether other `ITestHostEnvironmentVariableProvider` extensions can modify this value. + +`ValidateTestHostEnvironmentVariablesAsync`: This method is invoked after all the `UpdateAsync` methods of the registered `ITestHostEnvironmentVariableProvider` instances have been called. It allows you to *verify* the correct setup of the environment variables. It takes an object that implements `IReadOnlyEnvironmentVariables`, which provides the `TryGetVariable` method to fetch specific environment variable information with the `OwnedEnvironmentVariable` object type. After validation, you return a `ValidationResult` containing any failure reasons. + +> [!NOTE] +> The testing platform, by default, implements and registers the `SystemEnvironmentVariableProvider`. This provider loads all the *current* environment variables. As the first registered provider, it executes first, granting access to the default environment variables for all other `ITestHostEnvironmentVariableProvider` user extensions. + +If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](asyncinitcleanup.md). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](compositeextensionfactory.md) section. + +### The `ITestHostProcessLifetimeHandler` extensions + +The `ITestHostProcessLifetimeHandler` is an *out-of-process* extension that allows you to observe the test host process from an external standpoint. This ensures that your extension remains unaffected by potential crashes or hangs that could be induced by the code under test. Utilizing this extension point will prompt the testing platform to initiate a new host, as detailed in the [architecture](./unit-testing-platform-architecture.md) section. + +To register a custom `ITestHostProcessLifetimeHandler`, utilize the following API: + +```cs +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +... +testApplicationBuilder.TestHostControllers.AddProcessLifetimeHandler(serviceProvider => + new CustomMonitorTestHost()); +... +``` + +The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. + +> [!IMPORTANT] +> The sequence of registration is significant, as the APIs are called in the order they were registered. + +The `ITestHostProcessLifetimeHandler` interface includes the following methods: + +```cs +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; } +} +``` + +The `ITestHostProcessLifetimeHandler` is a type of `ITestHostControllersExtension`, which serves as a base for all *test host controller* extensions. Like all other extension points, it also inherits from [IExtension](#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. + +Let's describe the api: + +`BeforeTestHostProcessStartAsync`: This method is invoked prior to the testing platform initiating the test hosts. + +`OnTestHostProcessStartedAsync`: This method is invoked immediately after the test host starts. This method offers an object that implements the `ITestHostProcessInformation` interface, which provides key details about the test host process result. +> [!IMPORTANT] +> The invocation of this method does not halt the test host's execution. If you need to pause it, you should register an [*in-process*](extensionintro.md) extension such as [`ITestApplicationLifecycleCallbacks`](itestapplicationlifecyclecallbacks.md) and synchronize it with the *out-of-process* extension. + +`OnTestHostProcessExitedAsync`: This method is invoked when the test suite execution is complete. This method supplies an object that adheres to the `ITestHostProcessInformation` interface, which conveys crucial details about the outcome of the test host process. + +The `ITestHostProcessInformation` interface provides the following details: + +* `PID`: The process ID of the test host. +* `ExitCode`: The exit code of the process. This value is only available within the `OnTestHostProcessExitedAsync` method. Attempting to access it within the `OnTestHostProcessStartedAsync` method will result in an exception. +* `HasExitedGracefully`: A boolean value indicating whether the test host has crashed. If true, it signifies that the test host did not exit gracefully. + +## Extensions execution order + +The testing platform consists of a [testing framework](#test-framework-extension) and any number of extensions that can operate [*in-process*](extensionintro.md) or [*out-of-process*](extensionintro.md). This document outlines the **sequence of calls** to all potential extensibility points to provide clarity on when a feature is anticipated to be invoked. + +While a *sequence* could be used to depict this, we opt for a straightforward order of invocation calls, which allows for a more comprehensive commentary on the workflow. + +1. [ITestHostEnvironmentVariableProvider.UpdateAsync](itesthostenvironmentvariableprovider.md) : Out-of-process +1. [ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync](itesthostenvironmentvariableprovider.md) : Out-of-process +1. [ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync](itesthostprocesslifetimehandler.md) : Out-of-process +1. Test host process start +1. [ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync](itesthostprocesslifetimehandler.md) : Out-of-process, this event can intertwine the actions of *in-process* extensions, depending on race conditions. +1. [ITestApplicationLifecycleCallbacks.BeforeRunAsync](itestsessionlifetimehandler.md): In-process +1. [ITestSessionLifetimeHandler.OnTestSessionStartingAsync](itestsessionlifetimehandler.md): In-process +1. [ITestFramework.CreateTestSessionAsync](#test-framework-extension): In-process +1. [ITestFramework.ExecuteRequestAsync](#test-framework-extension): In-process, this method can be called one or more times. At this point, the testing framework will transmit information to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) that can be utilized by the [IDataConsumer](idataconsumer.md). +1. [ITestFramework.CloseTestSessionAsync](#test-framework-extension): In-process +1. [ITestSessionLifetimeHandler.OnTestSessionFinishingAsync](itestsessionlifetimehandler.md): In-process +1. [ITestApplicationLifecycleCallbacks.AfterRunAsync](itestsessionlifetimehandler.md): In-process +1. In-process cleanup, involves calling dispose and [IAsyncCleanableExtension](asyncinitcleanup.md) on all extension points. +1. [ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync](itesthostprocesslifetimehandler.md) : Out-of-process +1. Out-of-process cleanup, involves calling dispose and [IAsyncCleanableExtension](asyncinitcleanup.md) on all extension points. + +## Extensions helpers + +### Asynchronous initialization and cleanup of extensions + +The creation of the testing framework and extensions through factories adheres to the standard .NET object creation mechanism, which uses synchronous constructors. If an extension requires intensive initialization (such as accessing the file system or network), it cannot employ the *async/await* pattern in the constructor because constructors return void, not `Task`. + +Therefore, the testing platform provides a method to initialize an extension using the async/await pattern through a simple interface. For symmetry, it also offers an async interface for cleanup that extensions can implement seamlessly. + +```cs +public interface IAsyncInitializableExtension +{ + Task InitializeAsync(); +} + +public interface IAsyncCleanableExtension +{ + Task CleanupAsync(); +} +``` + +`IAsyncInitializableExtension.InitializeAsync`: This method is assured to be invoked following the creation factory. + +`IAsyncCleanableExtension.CleanupAsync`: This method is assured to be invoked *at least one time* during the termination of the testing session, prior to the default `DisposeAsync` or `Dispose`. + +> [!IMPORTANT] +> Similar to the standard `Dispose` method, `CleanupAsync` may be invoked multiple times. If an object's `CleanupAsync` method is called more than once, the object must ignore all calls after the first one. The object must not throw an exception if its `CleanupAsync` method is called multiple times. +> [!NOTE] +> By default, the testing platform will call `DisposeAsync` if it's available, or `Dispose` if it's implemented. It's important to note that the testing platform will not call both dispose methods but will prioritize the async one if implemented. + +### The CompositeExtensionFactory + +As outlined in the [extensions](#microsofttestingplatform-extensions) section, the testing platform enables you to implement interfaces to incorporate custom extensions both in and out of process. + +Each interface addresses a particular feature, and according to .NET design, you implement this interface in a specific object. You can register the extension itself using the specific registration API `AddXXX` from the `TestHost` or `TestHostController` object from the `ITestApplicationBuilder` as detailed in the corresponding sections. + +However, if you need to *share state* between two extensions, the fact that you can implement and register different objects implementing different interfaces makes sharing a challenging task. Without any assistance, you would need a way to pass one extension to the other to share information, which complicates the design. + +Hence, the testing platform provides a sophisticated method to implement multiple extension points using the same type, making data sharing a straightforward task. All you need to do is utilize the `CompositeExtensionFactory`, which can then be registered using the same API as you would for a single interface implementation. + +For instance, consider a type that implements both `ITestSessionLifetimeHandler` and `IDataConsumer`. This is a common scenario because you often want to gather information from the [testing framework](#test-framework-extension) and then, when the testing session concludes, you'll dispatch your artifact using the [`IMessageBus`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) within the `ITestSessionLifetimeHandler.OnTestSessionFinishingAsync`. + +What you should do is to normally implement the interfaces: + +```cs +internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ... +{ + ... +} +``` + +Once you've created the `CompositeExtensionFactory` for your type, you can register it with both the `IDataConsumer` and `ITestSessionLifetimeHandler` APIs, which offer an overload for the `CompositeExtensionFactory`: + +```cs +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +... +CompositeExtensionFactory compositeExtensionFactory = new(serviceProvider => new CustomExtension()); +testApplicationBuilder.TestHost.AddTestSessionLifetimeHandle(compositeExtensionFactory); +testApplicationBuilder.TestHost.AddDataConsumer(compositeExtensionFactory); +... +``` + +The factory constructor employs the [IServiceProvider](./unit-testing-platform-architecture-services.md) to access the services provided by the testing platform. + +The testing platform will be responsible for managing the lifecycle of the composite extension. + +It's important to note that due to the testing platform's support for both *in-process* and *out-of-process* extensions, you can't combine any extension point arbitrarily. The creation and utilization of extensions are contingent on the host type, meaning you can only group *in-process* (TestHost) and *out-of-process* (TestHostController) extensions together. + +The following combinations are possible: + +* For `ITestApplicationBuilder.TestHost`, you can combine `IDataConsumer` and `ITestSessionLifetimeHandler`. +* For `ITestApplicationBuilder.TestHostControllers`, you can combine `ITestHostEnvironmentVariableProvider` and `ITestHostProcessLifetimeHandler`. diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md new file mode 100644 index 0000000000000..270dd94f70d42 --- /dev/null +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -0,0 +1,357 @@ +--- +title: Microsoft.Testing.Platform architecture overview +description: Learn about how to extend Microsoft.Testing.Platform. +author: MarcoRossignoli +ms.author: mrossignoli +ms.date: 07/11/2024 +--- + +# Microsoft.Testing.Platform Services + +The testing platform offers valuable services to both the testing framework and extension points. These services cater to common needs such as accessing the configuration, parsing and retrieving command-line arguments, obtaining the logging factory, and accessing the logging system, among others. `IServiceProvider` implements the [service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern) for the testing platform. + +The `IServiceProvider` is derived directly from the base class library. + +```cs +namespace System +{ + public interface IServiceProvider + { + object? GetService(Type serviceType); + } +} +``` + +The testing platform offers handy extension methods to access well-known service objects. All these methods are housed in a static class within the `Microsoft.Testing.Platform.Services` namespace. + +```cs +public static class ServiceProviderExtensions +{ + public static TService GetRequiredService(this IServiceProvider provider) + public static TService? GetService(this IServiceProvider provider) + public static IMessageBus GetMessageBus(this IServiceProvider serviceProvider) + public static IConfiguration GetConfiguration(this IServiceProvider serviceProvider) + public static ICommandLineOptions GetCommandLineOptions(this IServiceProvider serviceProvider) + public static ILoggerFactory GetLoggerFactory(this IServiceProvider serviceProvider) + public static IOutputDevice GetOutputDevice(this IServiceProvider serviceProvider) + ...and more +} +``` + +Most of the registration factories exposed by extension points, which can be registered using the `ITestApplicationBuilder` during the setup of the testing application, provide access to the `IServiceProvider`. + +For example, we encountered it earlier when discussing [registering the testing framework](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework). + +```cs +ITestApplicationBuilder RegisterTestFramework( + Func capabilitiesFactory, + Func adapterFactory); +``` + +As observed above, both the `capabilitiesFactory` and the `adapterFactory` supply the `IServiceProvider` as a parameter. + +## The `IConfiguration` service + +The `IConfiguration` interface can be retrieved using the [`IServiceProvider`](#services) and provides access to the configuration settings for the testing framework and any extension points. By default, these configurations are loaded from: + +* Environment variables +* A JSON file named `[assemblyName].testingplatformconfig.json` located near the entry point assembly. + +**The order of precedence is maintained, which means that if a configuration is found in the environment variables, the JSON file will not be processed.** + +The interface is a straightforward key-value pair of strings: + +```cs +public interface IConfiguration +{ + string? this[string key] { get; } +} +``` + +### JSON configuration file + +The JSON file follows a hierarchical structure. To access child properties, you need to use the `:` separator. For example, consider a configuration for a potential testing framework like: + +```json +{ + "CustomTestingFramework": { + "DisableParallelism": true + } +} +``` + +The code snippet would look something like this: + +```cs +IServiceProvider serviceProvider = ...get the service provider... +IConfiguration configuration = serviceProvider.GetConfiguration(); +if (configuration["CustomTestingFramework:DisableParallelism"] == bool.TrueString) +{ + ... +} +``` + +In the case of an array, such as: + +```json +{ + "CustomTestingFramework": { + "Engine": [ + "ThreadPool", + "CustomThread" + ] + } +} +``` + +The syntax to access to the fist element ("ThreadPool") is: + +```cs +IServiceProvider serviceProvider = ...get the service provider... +IConfiguration configuration = serviceProvider.GetConfiguration(); +var fistElement = configuration["CustomTestingFramework:Engine:0"]; +``` + +### Environment variables + +The `:` separator doesn't work with environment variable hierarchical keys on all platforms. `__`, the double underscore, is: + +* Supported by all platforms. For example, the `:` separator is not supported by [Bash](https://linuxhint.com/bash-environment-variables/), but `__` is. +* Automatically replaced by a `:` + +For instance, the environment variable can be set as follows (This example is applicable for Windows): + +```bash +setx CustomTestingFramework__DisableParallelism=True +``` + +You can choose not to use the environment variable configuration source when creating the `ITestApplicationBuilder`: + +```cs +var testApplicationOptions = new TestApplicationOptions(); +testApplicationOptions.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource = false; +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args, testApplicationOptions); +``` + +## The `ICommandLineOptions` service + +The `ICommandLineOptions` service is utilized to fetch details regarding the command-line options that the platform has parsed. The APIs available include: + +```cs +public interface ICommandLineOptions +{ + bool IsOptionSet(string optionName); + bool TryGetOptionArgumentList(string optionName, out string[]? arguments); +} +``` + +The `ICommandLineOptions` can be obtained through certain APIs, such as the [ICommandLineOptionsProvider](./unit-testing-platform-architecture-extensions.md#the-icommandlineoptionsprovider-extensions), or you can retrieve an instance of it from the [IServiceProvider](#services) via the extension method `serviceProvider.GetCommandLineOptions()`. + +`ICommandLineOptions.IsOptionSet(string optionName)`: This method allows you to verify whether a specific option has been specified. When specifying the `optionName`, omit the `--` prefix. For example, if the user inputs `--myOption`, you should simply pass `myOption`. + +`ICommandLineOptions.TryGetOptionArgumentList(string optionName, out string[]? arguments)`: This method enables you to check whether a specific option has been set and, if so, retrieve the corresponding value or values (if the arity is more than one). Similar to the previous case, the `optionName` should be provided without the `--` prefix. + +### The `ILoggerFactory` service + +The testing platform comes with an integrated logging system that generates a log file. You can view the logging options by running the `--help` command. +The options you can choose from include: + +```dotnetcli +--diagnostic Enable the diagnostic logging. The default log level is 'Trace'. The file will be written in the output directory with the name log_[MMddHHssfff].diag +--diagnostic-filelogger-synchronouswrite Force the built-in file logger to write the log synchronously. Useful for scenario where you don't want to lose any log (i.e. in case of crash). Note that this is slowing down the test execution. +--diagnostic-output-directory Output directory of the diagnostic logging, if not specified the file will be generated inside the default 'TestResults' directory. +--diagnostic-output-fileprefix Prefix for the log file name that will replace '[log]_.' +--diagnostic-verbosity Define the level of the verbosity for the --diagnostic. The available values are 'Trace', 'Debug', 'Information', 'Warning', 'Error', and 'Critical' +``` + +From a coding standpoint, to log information, you need to obtain the `ILoggerFactory` from the [`IServiceProvider`](iserviceprovider.md). +The `ILoggerFactory` API is as follows: + +```cs +public interface ILoggerFactory +{ + ILogger CreateLogger(string categoryName); +} + +public static class LoggerFactoryExtensions +{ + public static ILogger CreateLogger(this ILoggerFactory factory); +} +``` + +The logger factory allows you to create an `ILogger` object using the `CreateLogger` API. There's also a convenient API that accepts a generic argument, which will be used as the category name. + +```cs +public interface ILogger +{ + Task LogAsync(LogLevel logLevel, TState state, Exception? exception, Func formatter); + void Log(LogLevel logLevel, TState state, Exception? exception, Func formatter); + bool IsEnabled(LogLevel logLevel); +} + +public interface ILogger : ILogger +{ +} + +public static class LoggingExtensions +{ + public static Task LogTraceAsync(this ILogger logger, string message); + public static Task LogDebugAsync(this ILogger logger, string message); + public static Task LogInformationAsync(this ILogger logger, string message); + public static Task LogWarningAsync(this ILogger logger, string message); + public static Task LogErrorAsync(this ILogger logger, string message); + public static Task LogErrorAsync(this ILogger logger, string message, Exception ex); + public static Task LogErrorAsync(this ILogger logger, Exception ex); + public static Task LogCriticalAsync(this ILogger logger, string message); + public static void LogTrace(this ILogger logger, string message); + public static void LogDebug(this ILogger logger, string message); + public static void LogInformation(this ILogger logger, string message); + public static void LogWarning(this ILogger logger, string message); + public static void LogError(this ILogger logger, string message); + public static void LogError(this ILogger logger, string message, Exception ex); + public static void LogError(this ILogger logger, Exception ex); + public static void LogCritical(this ILogger logger, string message); +} +``` + +The `ILogger` object, which is created by the `ILoggerFactory`, offers APIs for logging information at various levels. These logging levels include: + +```cs +public enum LogLevel +{ + Trace, + Debug, + Information, + Warning, + Error, + Critical, + None +} +``` + +Here's an example of how you might use the logging API: + +```cs +... +IServiceProvider serviceProvider = ...get the service provider... +ILoggerFactory loggerFactory = serviceProvider.GetLoggerFactory(); +ILogger logger = loggerFactory.CreateLogger(); +... +if (_logger.IsEnabled(LogLevel.Information)) +{ + await _logger.LogInformationAsync($"Executing request of type '{context.Request}'"); +} +... +``` + +Keep in mind that to prevent unnecessary allocation, you should check if the level is *enabled* using the `ILogger.IsEnabled(LogLevel)` API. + +## The `IMessageBus` service + +The message bus service is the central mechanism that facilitates information exchange between the test framework and its extensions. + +The message bus of the testing platform employs the publish-subscribe pattern, as described here: . + +The overarching structure of the shared bus is as follows: + +![bus](bus.png) + +As illustrated in the diagram, which includes an extensions and a test framework, there are two potential actions: pushing information to the bus or consuming information from the bus. + +The `IMessageBus` satisfied the *pushing action* to the bus and the api is: + +```cs +public interface IMessageBus +{ + Task PublishAsync(IDataProducer dataProducer, IData data); +} + +public interface IDataProducer : IExtension +{ + Type[] DataTypesProduced { get; } +} + +public interface IData +{ + string DisplayName { get; } + string? Description { get; } +} +``` + +Let's discuss the parameters: + +* `IDataProducer`: The `IDataProducer` communicates to the message bus the `Type` of information it can supply and establishes ownership through inheritance from the base interface [IExtension](./unit-testing-platform-architecture-extensions.md#the-iextension-interface). This implies that you can't indiscriminately push data to the message bus; you must declare the data type produced in advance. If you push unexpected data, an exception will be triggered. + +* `IData`: This interface serves as a placeholder where you only need to provide descriptive details such as the name and a description. The interface doesn't reveal much about the data's nature, which is intentional. It implies that the test framework and extensions can push any type of data to the bus, and this data can be consumed by any registered extension or the test framework itself. +This approach facilitates the evolution of the information exchange process, preventing breaking changes when an extension is unfamiliar with new data. **It allows different versions of extensions and the test framework to operate in harmony, based on their mutual understanding**. + +The opposite end of the bus is what we refer to as a [consumer](./unit-testing-platform-architecture-extensions.md#the-idataconsumer-extensions), which is subscribed to a specific type of data and can thus consume it. + +> [!IMPORTANT] +> Always use *await* the call to `PublishAsync`. If you don't, the `IData` might not be processed correctly by the testing platform and extensions, which could lead to subtle bugs. It's only after you've returned from the *await* that you can be assured that the `IData` has been queued for processing on the message bus. Regardless of the extension point you're working on, ensure that you've awaited all `PublishAsync` calls before exiting the extension. For example, if you're implementing the [`testing framework`](./unit-testing-platform-architecture-extensions.md#creating-a-testing-framework), you should not call `Complete` on the [requests](./unit-testing-platform-architecture-extensions.md#handling-requests) until you've awaited all `PublishAsync` calls for that specific request. + +## The `IOutputDevice` service + +The testing platform encapsulates the idea of an *output device*, allowing the testing framework and extensions to *present* information by transmitting any kind of data to the currently utilized display system. + +The most traditional example of an *output device* is the console output. + +> [!NOTE] +> While the testing platform is engineered to support custom *output devices*, currently, this extension point is not available. + +To transmit data to the *output device*, you must obtain the `IOutputDevice` from the [`IServiceProvider`](#services). +The API consists of: + +```cs +public interface IOutputDevice +{ + Task DisplayAsync(IOutputDeviceDataProducer producer, IOutputDeviceData data); +} + +public interface IOutputDeviceDataProducer : IExtension +{ +} + +public interface IOutputDeviceData +{ +} +``` + +The `IOutputDeviceDataProducer` extends the [`IExtension`](./unit-testing-platform-architecture-extensions.md#the-iextension-interface) and provides information about the sender to the *output device*. +The `IOutputDeviceData` serves as a placeholder interface. The concept behind `IOutputDevice` is to accommodate more intricate information than just colored text. For instance, it could be a complex object that can be graphically represented. + +The testing platform, by default, offers a traditional colored text model for the `IOutputDeviceData` object: + +```cs +public class TextOutputDeviceData : IOutputDeviceData +{ + public TextOutputDeviceData(string text) + public string Text { get; } +} + +public sealed class FormattedTextOutputDeviceData : TextOutputDeviceData +{ + public FormattedTextOutputDeviceData(string text) + public IColor? ForegroundColor { get; init; } + public IColor? BackgroundColor { get; init; } +} + +public sealed class SystemConsoleColor : IColor +{ + public ConsoleColor ConsoleColor { get; init; } +} +``` + +Here's an example of how you might use the colored text with the *active* output device: + +```cs +IServiceProvider serviceProvider = ...get the service provider... +IOutputDevice outputDevice = serviceProvider.GetOutputDevice(); +await outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"TestingFramework version '{Version}' running tests with parallelism of {_dopValue}") { ForegroundColor = new SystemConsoleColor() { ConsoleColor = ConsoleColor.Green } }); +``` + +Beyond the standard use of colored text, the main advantage of `IOutputDevice` and `IOutputDeviceData` is that the *output device* is entirely independent and unknown to the user. This allows for the development of complex user interfaces. For example, it's entirely feasible to implement a *real-time* web application that displays the progress of tests. + +## The `IPlatformInformation` service + +Provides information about the platform such as: name, version, commit hash and build date. diff --git a/docs/core/testing/unit-testing-platform-architecture.md b/docs/core/testing/unit-testing-platform-architecture.md new file mode 100644 index 0000000000000..6a036f68946b7 --- /dev/null +++ b/docs/core/testing/unit-testing-platform-architecture.md @@ -0,0 +1,154 @@ +--- +title: Microsoft.Testing.Platform architecture overview +description: Learn about how to extend Microsoft.Testing.Platform. +author: MarcoRossignoli +ms.author: mrossignoli +ms.date: 07/11/2024 +--- + +# Microsoft.Testing.Platform architecture + +Welcome to our new test platform! To help you get acquainted with its capabilities, we'll start with a simple example that demonstrates how to register and run a test. This foundational example will give you a solid understanding of the core functionality and how to get started quickly. + +[Step 1: Register and Run a simple test application](#step-1-register-and-run-a-simple-test-application) + +In this initial example, we will walk you through the basic steps to declare and run a test application. This straightforward approach ensures that you can immediately start using the platform with minimal setup. + +[Step 2: Extending the Platform](#step-2-extending-the-platform) + +After you've discovered how to create your first test application, we will explore an example of extension to cover partially the concepts surrounding the test application extensions. + +[Step 3: Comprehensive Overview of Extension Points](#step-3-comprehensive-overview-of-extension-points) + +Once you're comfortable with the basics, we'll delve into the various extension points. This will include: + +1. **Platform and Test Framework Capabilities**: Understanding the full range of capabilities provided by the platform and the test framework, allowing you to leverage them effectively. + +1. **Custom Test Framework**: How to write and register your custom test framework, enabling you to tailor the testing environment to your specific requirements. + +1. **In-Process and Out-of-Process Extensions**: Detailed instructions on how to write and register both in-process and out-of-process extensions, offering flexibility in how you extend the platform. + +1. **Order of Execution**: Clarifying the order of execution for the various extension points to ensure seamless integration and operation of your custom extensions. + +[Step 4: Available Services and Helpers](#step-4-available-services) + +Finally, we will provide an exhaustive list of the available services and helper functions within the platform. This section will serve as a reference to help you leverage all the tools at your disposal for creating robust and efficient test extensions. + +By following this structured approach, you will gain a comprehensive understanding of our test platform, from basic usage to advanced customization. Let's get started and explore the full potential of what our platform can offer! + +## Step 1: Register and Run a simple test application + +To introduce the architecture of the testing platform, we will use the classic console application (for Windows) as the host. The samples in this document are written in C#, but you can use the testing platform with any language that supports the .NET Ecma specification, and run on any OS supported by .NET. To use the platform, simply reference the `Microsoft.Testing.Platform.dll` assembly, which can be consumed through the official NuGet package available at . + +In a console project `Contoso.UnitTests.exe` the following `Main` method defines the entry point: + +```cs +public static async Task Main(string[] args) +{ + ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); + testApplicationBuilder.RegisterTestFramework(/* test framework registration factories */); + using ITestApplication testApplication = await testApplicationBuilder.BuildAsync(); + return await testApplication.RunAsync(); +} +``` + +This code includes everything needed to execute a test session, except for registering a test framework such as MSTest through `RegisterTestFramework`. This code is shown and explained in later sections. + +Please also note that in a typical setup this code is automatically generated through MSBuild, and is not visible in your project. By typical setup we mean for example generating new project from .NET 9 `mstest` template via `dotnet new mstest`. + +When `Contoso.UnitTests.exe` application is started a standard Windows process is created, and the testing platform interacts with the registered testing framework to execute the testing session. + +A single process is created to carry out this work: + +```mermaid +graph TD; + TestHost:'Contoso.UnitTests.exe'; +``` + +The testing platform includes a built-in display device that writes the testing session information in the terminal, similar to: + +```bash +Microsoft(R) Testing Platform Execution Command Line Tool +Version: 1.1.0+8c0a8fd8e (UTC 2024/04/03) +RuntimeInformation: win-x64 - .NET 9.0.0-preview.1.24080.9 +Copyright(c) Microsoft Corporation.  All rights reserved. +Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 5ms - Contoso.UnitTests.dll (win-x64 - .NET 9.0.0-preview.1.24080.9) +``` + +> [!NOTE] +> The known exit codes returned by the `ITestApplication.RunAsync()` call are detailed in [platform exit codes](./unit-testing-platform-exit-codes.md). + +## Step 2: Extending the platform + +Test runs commonly collect code coverage information, or similar information to evaluate code quality. Such workloads may require configuration to be done before the test host process starts, for example setting environment variables. + +The testing platform accommodates this by having **out-of-process** extensions. When running with an out-of-process extensions, the testing platform will start multiple processes and it will manage them appropriately. + +The following example demonstrates how to register a code coverage feature using a **TestHostController** extension. + +```cs +ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); +testApplicationBuilder.RegisterTestFramework(/* test framework registration factories */); +testApplicationBuilder.AddCodeCoverage(); +using ITestApplication testApplication = await testApplicationBuilder.BuildAsync(); +return await testApplication.RunAsync(); +``` + +The `testApplicationBuilder.AddCodeCoverage();` internally uses the **TestHostController** extensibility point, which is an out-of-process extensibility point. + +```cs +public static class TestApplicationBuilderExtensions +{ + public static ITestApplicationBuilder AddCodeCoverage(this ITestApplicationBuilder testApplicationBuilder) + { + testApplicationBuilder.TestHostControllers.AddEnvironmentVariableProvider(...); + .... + return testApplicationBuilder; + } +} +``` + +The parameters for the api `AddEnvironmentVariableProvider` will be explained in later sections. + +When we run `Contoso.UnitTests.exe` this time, the testing platform detects that a `TestHostController` extension is registered. As a result, it starts another instance of the `Contoso.UnitTests.exe` process as a child process. This is done to properly set the environment variables as required by the extension registered with the `AddEnvironmentVariableProvider` API. + +The process layout looks like this: + +```mermaid +graph TD; + TestHostController:'Contoso.UnitTests.exe'-->TestHost:'Contoso.UnitTests.exe; +``` + +> [!NOTE] +> The provided example assumes a console application layout, which handles the start process correctly and propagates all command line arguments to the child process. +> If you are using a different host, you need to ensure that the entry point code correctly forwards the process entry point (the "Main") to the appropriate code block. +> The runtime simply starts itself with the same command line arguments. + +The above section provides a brief introduction to the architecture of the testing platform. The current extensibility points are divided into two categories: + +1. **In process** extensions can be accessed through the `TestHost` property of the test application builder. In process means that they will run in the same process as the test framework. + + ```cs + ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); + testApplicationBuilder.RegisterTestFramework(/* test framework registration factories */); + testApplicationBuilder.TestHost.AddXXX(...); + ``` + + As observed, the most crucial extension point is the in-process *testing framework* (`RegisterTestFramework`), which is the only **mandatory** one. + +1. **Out of process** extensions can be accessed through the `TestHostControllers` property of the test application builder. These extensions run in a separate process from the test framework to "observe" it. + + ```cs + ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); + testApplicationBuilder.TestHostControllers.AddXXX(...); + ``` + +## Step 3: Comprehensive Overview of Extension points + +Let's start by getting familiar with the concept of [capabilities](./unit-testing-platform-architecture-capabilities.md) before diving into the various [extensions points](./unit-testing-platform-architecture-extensions.md). + +## Step 4: Available services + +The testing platform offers valuable services to both the testing framework and extension points. These services cater to common needs such as accessing the configuration, parsing and retrieving command-line arguments, obtaining the logging factory, and accessing the logging system, among others. `IServiceProvider` implements the [service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern) for the testing platform. + +All the services, helpers and technical information about how to access and use these services is listed [here](./unit-testing-platform-architecture-services.md). diff --git a/docs/core/testing/unit-testing-platform-exit-codes.md b/docs/core/testing/unit-testing-platform-exit-codes.md index 2b29df0d99c38..7bdf4210bfa30 100644 --- a/docs/core/testing/unit-testing-platform-exit-codes.md +++ b/docs/core/testing/unit-testing-platform-exit-codes.md @@ -19,7 +19,7 @@ ms.topic: reference | `3` | The exit code `3` indicates that the test session was aborted. A session can be aborted using Ctrl+C, as an example. | | `4` | The exit code `4` indicates that the setup of used extensions is invalid and the tests session cannot run. | | `5` | The exit code `5` indicates that the command line arguments passed to the test app are invalid. | -| `6` | The exit code `6` indicates that the test session is using a nonimplemented feature. | +| `6` | The exit code `6` indicates that the test session is using a non-implemented feature. | | `7` | The exit code `7` indicates that a test session was unable to complete successfully, and likely crashed. It's possible that this was caused by a test session that was run via a test controller's extension point. | | `8` | The exit code `8` indicates that the test session ran zero tests. | | `9` | The exit code `9` indicates that the minimum execution policy for the executed tests was violated. | diff --git a/docs/core/testing/unit-testing-platform-intro.md b/docs/core/testing/unit-testing-platform-intro.md index 722a484aa09ef..df5ee2cd8c057 100644 --- a/docs/core/testing/unit-testing-platform-intro.md +++ b/docs/core/testing/unit-testing-platform-intro.md @@ -12,6 +12,30 @@ Microsoft.Testing.Platform is a lightweight and portable alternative to [VSTest] `Microsoft.Testing.Platform` is open source. You can find `Microsoft.Testing.Platform` code in [microsoft/testfx](https://github.com/microsoft/testfx/tree/main/src/Platform/Microsoft.Testing.Platform) GitHub repository. +## Microsoft.Testing.Platform pillars + +This new testing platform is built on the .NET Developer Experience Testing team experience and aims at addressing the challenges encountered since the release of .NET Core in 2016. While there is a high level of compatibility between the .NET Framework and the .NET Core/.NET, some key features like the plugin system and the new possible form factors of .NET compilations have made it complex to evolve or fully support the new runtime feature with the current [VSTest platform](https://github.com/microsoft/vstest) architecture. + +The main driving factors for the evolution of the new testing platform are: + +* **Determinism**: Ensuring that running the same tests in different contexts (local, CI) will produce the same result. The new runtime does not rely on reflection or any other dynamic .NET runtime feature to coordinate a test run. + +* **Runtime transparency**: The test runtime does not interfere with the test framework code, it does not create isolated contexts like `AppDomain` or `AssemblyLoadContext`, and it does not use reflection or custom assembly resolvers. + +* **Compile-time registration of extensions**: Extensions, such as test frameworks and in/out-of-process extensions, are registered during compile-time to ensure determinism and to facilitate detection of inconsistencies. + +* **0 dependencies**: The core of the platform is a single .NET assembly, `Microsoft.Testing.Platform.dll`, which has no dependencies other than the supported runtimes. + +* **Hostable**: The test runtime can be hosted in any .NET application. While a console application is commonly used to run tests, you can create a test application in any type of .NET application. This allows you to run tests within special contexts, such as devices or browsers, where there may be limitations. + +* **Support all .NET form factors**: Support current and future .NET form factors, including Native AOT. + +* **Performant**: Finding the right balance between features and extension points to avoid bloating the runtime with non-fundamental code. The new test platform is designed to "orchestrate" a test run, rather than providing implementation details on how to do it. + +* **Extensible enough**: The new platform is built on extensibility points to allow for maximum customization of runtime execution. It allows you to configure the test process host, observe the test process, and consume information from the test framework within the test host process. + +* **Single module deploy**: The hostability feature enables a single module deploy model, where a single compilation result can be used to support all extensibility points, both out-of-process and in-process, without the need to ship different executable modules. + ## Supported test frameworks * MSTest. In MSTest, the support of `Microsoft.Testing.Platform` is done via [MSTest runner](unit-testing-mstest-runner-intro.md). diff --git a/docs/navigate/devops-testing/toc.yml b/docs/navigate/devops-testing/toc.yml index 81c46cd43cf37..ccbecd49da1f4 100644 --- a/docs/navigate/devops-testing/toc.yml +++ b/docs/navigate/devops-testing/toc.yml @@ -179,8 +179,16 @@ items: href: ../../core/testing/unit-testing-platform-exit-codes.md - name: Integration with dotnet test href: ../../core/testing/unit-testing-platform-integration-dotnet-test.md - - name: Testing platform SDK - href: https://github.com/microsoft/testfx/blob/main/docs/testingplatform/Index.md + - name: Testing platform Architecture + items: + - name: Overview + href: ../../core/testing/unit-testing-platform-architecture.md + - name: Capabilities + href: ../../core/testing/unit-testing-platform-architecture-capabilities.md + - name: Extensions + href: ../../core/testing/unit-testing-platform-architecture-extensions.md + - name: Services + href: ../../core/testing/unit-testing-platform-architecture-services.md - name: Run selective unit tests href: ../../core/testing/selective-unit-tests.md - name: Order unit tests From 8e7e959466373121b87101393b36be4becd6cacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jul 2024 10:53:55 +0200 Subject: [PATCH 02/19] Fix links and png --- docs/core/testing/media/bus.png | Bin 0 -> 51343 bytes ...esting-platform-architecture-extensions.md | 42 +++++++++--------- ...-testing-platform-architecture-services.md | 10 ++--- 3 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 docs/core/testing/media/bus.png diff --git a/docs/core/testing/media/bus.png b/docs/core/testing/media/bus.png new file mode 100644 index 0000000000000000000000000000000000000000..b325c10bbfa41ff7d65dd4ad36363a01a666718d GIT binary patch literal 51343 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 literal 0 HcmV?d00001 diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index 8e0d58ced8a9f..e64251067f34b 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -8,7 +8,7 @@ ms.date: 07/11/2024 # Microsoft.Testing.Platform extensions -The testing platform consists of a [testing framework](#test-framework) and any number of [extensions](#extensiblity-points) that can operate *in-process* or *out-of-process*. +The testing platform consists of a [testing framework](#test-framework-extension) and any number of [extensions](#other-extensibility-points) that can operate *in-process* or *out-of-process*. As outlined in the [architecture](./unit-testing-platform-architecture.md) section, the testing platform is designed to accommodate a variety of scenarios and extensibility points. The primary and essential extension is undoubtedly the [testing framework](#test-framework-extension) that our tests will utilize. Failing to register it will result in a startup error. **The [testing framework](#test-framework-extension) is the sole mandatory extension required to execute a testing session.** @@ -92,11 +92,11 @@ ITestApplicationBuilder RegisterTestFramework( The `RegisterTestFramework` API expects two factories: -1. `Func`: This is a lambda function that accepts an object implementing the [`IServiceProvider`](./unit-testing-platform-architecture-services.md) interface and returns an object implementing the [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-./unit-testing-platform-architecture-capabilities.md) interface. The [`IServiceProvider`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) provides access to platform services such as configurations, loggers, command line arguments, etc. +1. `Func`: This is a lambda function that accepts an object implementing the [`IServiceProvider`](./unit-testing-platform-architecture-services.md) interface and returns an object implementing the [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) interface. The [`IServiceProvider`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) provides access to platform services such as configurations, loggers, command line arguments, etc. The [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) interface is used to announce the capabilities supported by the testing framework to the platform and extensions. It allows the platform and extensions to interact correctly by implementing and supporting specific behaviors. For a better understanding of the [concept of capabilities](./unit-testing-platform-architecture-capabilities.md), refer to the respective section. -1. `Func`: This is a lambda function that takes in an [ITestFrameworkCapabilities](./unit-testing-platform-architecture-capabilities.md) object, which is the instance returned by the `Func`, and an [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to provide access to platform services once more. The expected return object is one that implements the [ITestFramework](#test-framework-extension) interface. The ITestFramework serves as the execution engine that discovers and runs tests, and communicates the results back to the testing platform. +2. `Func`: This is a lambda function that takes in an [ITestFrameworkCapabilities](./unit-testing-platform-architecture-capabilities.md) object, which is the instance returned by the `Func`, and an [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to provide access to platform services once more. The expected return object is one that implements the [ITestFramework](#test-framework-extension) interface. The ITestFramework serves as the execution engine that discovers and runs tests, and communicates the results back to the testing platform. The need for the platform to separate the creation of the [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) and the creation of the [ITestFramework](#test-framework-extension) is an optimization to avoid creating the test framework if the supported capabilities are not sufficient to execute the current testing session. @@ -798,7 +798,7 @@ Let's describe the api: Finally, both APIs take a `CancellationToken` which the extension is expected to honor. -If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](asyncinitcleanup.md). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](compositeextensionfactory.md) section. +If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](#asynchronous-initialization-and-cleanup-of-extensions). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](#the-compositeextensionfactoryt) section. ### The `ITestApplicationLifecycleCallbacks` extensions @@ -938,9 +938,9 @@ Finally, the api takes a `CancellationToken` which the extension is expected to > [!WARNING] -> When using `IDataConsumer` in conjunction with [ITestHostProcessLifetimeHandler](itestsessionlifetimehandler.md) within a [composite extension point](compositeextensionfactory.md), **it's crucial to disregard any data received post the execution of [ITestSessionLifetimeHandler.OnTestSessionFinishingAsync](itestsessionlifetimehandler.md)**. The `OnTestSessionFinishingAsync` is the final opportunity to process accumulated data and transmit new information to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service), hence, any data consumed beyond this point will not be *utilizable* by the extension. +> When using `IDataConsumer` in conjunction with [ITestHostProcessLifetimeHandler](#the-itestsessionlifetimehandler-extensions) within a [composite extension point](#the-compositeextensionfactoryt), **it's crucial to disregard any data received post the execution of [ITestSessionLifetimeHandler.OnTestSessionFinishingAsync](#the-itestsessionlifetimehandler-extensions)**. The `OnTestSessionFinishingAsync` is the final opportunity to process accumulated data and transmit new information to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service), hence, any data consumed beyond this point will not be *utilizable* by the extension. -If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](asyncinitcleanup.md). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](compositeextensionfactory.md) section. +If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](#asynchronous-initialization-and-cleanup-of-extensions). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](#the-compositeextensionfactoryt) section. ### The `ITestHostEnvironmentVariableProvider` extensions @@ -1012,7 +1012,7 @@ Let's describe the api: > [!NOTE] > The testing platform, by default, implements and registers the `SystemEnvironmentVariableProvider`. This provider loads all the *current* environment variables. As the first registered provider, it executes first, granting access to the default environment variables for all other `ITestHostEnvironmentVariableProvider` user extensions. -If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](asyncinitcleanup.md). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](compositeextensionfactory.md) section. +If your extension requires intensive initialization and you need to use the async/await pattern, you can refer to the [`Async extension initialization and cleanup`](#asynchronous-initialization-and-cleanup-of-extensions). If you need to *share state* between extension points, you can refer to the [`CompositeExtensionFactory`](#the-compositeextensionfactoryt) section. ### The `ITestHostProcessLifetimeHandler` extensions @@ -1059,7 +1059,7 @@ Let's describe the api: `OnTestHostProcessStartedAsync`: This method is invoked immediately after the test host starts. This method offers an object that implements the `ITestHostProcessInformation` interface, which provides key details about the test host process result. > [!IMPORTANT] -> The invocation of this method does not halt the test host's execution. If you need to pause it, you should register an [*in-process*](extensionintro.md) extension such as [`ITestApplicationLifecycleCallbacks`](itestapplicationlifecyclecallbacks.md) and synchronize it with the *out-of-process* extension. +> The invocation of this method does not halt the test host's execution. If you need to pause it, you should register an [*in-process*](#microsofttestingplatform-extensions) extension such as [`ITestApplicationLifecycleCallbacks`](#the-itestapplicationlifecyclecallbacks-extensions) and synchronize it with the *out-of-process* extension. `OnTestHostProcessExitedAsync`: This method is invoked when the test suite execution is complete. This method supplies an object that adheres to the `ITestHostProcessInformation` interface, which conveys crucial details about the outcome of the test host process. @@ -1071,25 +1071,25 @@ The `ITestHostProcessInformation` interface provides the following details: ## Extensions execution order -The testing platform consists of a [testing framework](#test-framework-extension) and any number of extensions that can operate [*in-process*](extensionintro.md) or [*out-of-process*](extensionintro.md). This document outlines the **sequence of calls** to all potential extensibility points to provide clarity on when a feature is anticipated to be invoked. +The testing platform consists of a [testing framework](#test-framework-extension) and any number of extensions that can operate [*in-process*](#microsofttestingplatform-extensions) or [*out-of-process*](#microsofttestingplatform-extensions). This document outlines the **sequence of calls** to all potential extensibility points to provide clarity on when a feature is anticipated to be invoked. While a *sequence* could be used to depict this, we opt for a straightforward order of invocation calls, which allows for a more comprehensive commentary on the workflow. -1. [ITestHostEnvironmentVariableProvider.UpdateAsync](itesthostenvironmentvariableprovider.md) : Out-of-process -1. [ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync](itesthostenvironmentvariableprovider.md) : Out-of-process -1. [ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync](itesthostprocesslifetimehandler.md) : Out-of-process +1. [ITestHostEnvironmentVariableProvider.UpdateAsync](#the-itesthostenvironmentvariableprovider-extensions) : Out-of-process +1. [ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync](#the-itesthostenvironmentvariableprovider-extensions) : Out-of-process +1. [ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync](#the-itestsessionlifetimehandler-extensions) : Out-of-process 1. Test host process start -1. [ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync](itesthostprocesslifetimehandler.md) : Out-of-process, this event can intertwine the actions of *in-process* extensions, depending on race conditions. -1. [ITestApplicationLifecycleCallbacks.BeforeRunAsync](itestsessionlifetimehandler.md): In-process -1. [ITestSessionLifetimeHandler.OnTestSessionStartingAsync](itestsessionlifetimehandler.md): In-process +1. [ITestHostProcessLifetimeHandler.OnTestHostProcessStartedAsync](#the-itestsessionlifetimehandler-extensions) : Out-of-process, this event can intertwine the actions of *in-process* extensions, depending on race conditions. +1. [ITestApplicationLifecycleCallbacks.BeforeRunAsync](#the-itestsessionlifetimehandler-extensions): In-process +1. [ITestSessionLifetimeHandler.OnTestSessionStartingAsync](#the-itestsessionlifetimehandler-extensions): In-process 1. [ITestFramework.CreateTestSessionAsync](#test-framework-extension): In-process -1. [ITestFramework.ExecuteRequestAsync](#test-framework-extension): In-process, this method can be called one or more times. At this point, the testing framework will transmit information to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) that can be utilized by the [IDataConsumer](idataconsumer.md). +1. [ITestFramework.ExecuteRequestAsync](#test-framework-extension): In-process, this method can be called one or more times. At this point, the testing framework will transmit information to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) that can be utilized by the [IDataConsumer](#the-idataconsumer-extensions). 1. [ITestFramework.CloseTestSessionAsync](#test-framework-extension): In-process -1. [ITestSessionLifetimeHandler.OnTestSessionFinishingAsync](itestsessionlifetimehandler.md): In-process -1. [ITestApplicationLifecycleCallbacks.AfterRunAsync](itestsessionlifetimehandler.md): In-process -1. In-process cleanup, involves calling dispose and [IAsyncCleanableExtension](asyncinitcleanup.md) on all extension points. -1. [ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync](itesthostprocesslifetimehandler.md) : Out-of-process -1. Out-of-process cleanup, involves calling dispose and [IAsyncCleanableExtension](asyncinitcleanup.md) on all extension points. +1. [ITestSessionLifetimeHandler.OnTestSessionFinishingAsync](#the-itestsessionlifetimehandler-extensions): In-process +1. [ITestApplicationLifecycleCallbacks.AfterRunAsync](#the-itestsessionlifetimehandler-extensions): In-process +1. In-process cleanup, involves calling dispose and [IAsyncCleanableExtension](#asynchronous-initialization-and-cleanup-of-extensions) on all extension points. +1. [ITestHostProcessLifetimeHandler.OnTestHostProcessExitedAsync](#the-itestsessionlifetimehandler-extensions) : Out-of-process +1. Out-of-process cleanup, involves calling dispose and [IAsyncCleanableExtension](#asynchronous-initialization-and-cleanup-of-extensions) on all extension points. ## Extensions helpers diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index 270dd94f70d42..ca97a44b1f8cb 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -52,7 +52,7 @@ As observed above, both the `capabilitiesFactory` and the `adapterFactory` suppl ## The `IConfiguration` service -The `IConfiguration` interface can be retrieved using the [`IServiceProvider`](#services) and provides access to the configuration settings for the testing framework and any extension points. By default, these configurations are loaded from: +The `IConfiguration` interface can be retrieved using the [`IServiceProvider`](#microsofttestingplatform-services) and provides access to the configuration settings for the testing framework and any extension points. By default, these configurations are loaded from: * Environment variables * A JSON file named `[assemblyName].testingplatformconfig.json` located near the entry point assembly. @@ -145,7 +145,7 @@ public interface ICommandLineOptions } ``` -The `ICommandLineOptions` can be obtained through certain APIs, such as the [ICommandLineOptionsProvider](./unit-testing-platform-architecture-extensions.md#the-icommandlineoptionsprovider-extensions), or you can retrieve an instance of it from the [IServiceProvider](#services) via the extension method `serviceProvider.GetCommandLineOptions()`. +The `ICommandLineOptions` can be obtained through certain APIs, such as the [ICommandLineOptionsProvider](./unit-testing-platform-architecture-extensions.md#the-icommandlineoptionsprovider-extensions), or you can retrieve an instance of it from the [IServiceProvider](#microsofttestingplatform-services) via the extension method `serviceProvider.GetCommandLineOptions()`. `ICommandLineOptions.IsOptionSet(string optionName)`: This method allows you to verify whether a specific option has been specified. When specifying the `optionName`, omit the `--` prefix. For example, if the user inputs `--myOption`, you should simply pass `myOption`. @@ -164,7 +164,7 @@ The options you can choose from include: --diagnostic-verbosity Define the level of the verbosity for the --diagnostic. The available values are 'Trace', 'Debug', 'Information', 'Warning', 'Error', and 'Critical' ``` -From a coding standpoint, to log information, you need to obtain the `ILoggerFactory` from the [`IServiceProvider`](iserviceprovider.md). +From a coding standpoint, to log information, you need to obtain the `ILoggerFactory` from the [`IServiceProvider`](#microsofttestingplatform-services). The `ILoggerFactory` API is as follows: ```cs @@ -254,7 +254,7 @@ The message bus of the testing platform employs the publish-subscribe pattern, a The overarching structure of the shared bus is as follows: -![bus](bus.png) +![bus](./media/bus.png) As illustrated in the diagram, which includes an extensions and a test framework, there are two potential actions: pushing information to the bus or consuming information from the bus. @@ -299,7 +299,7 @@ The most traditional example of an *output device* is the console output. > [!NOTE] > While the testing platform is engineered to support custom *output devices*, currently, this extension point is not available. -To transmit data to the *output device*, you must obtain the `IOutputDevice` from the [`IServiceProvider`](#services). +To transmit data to the *output device*, you must obtain the `IOutputDevice` from the [`IServiceProvider`](#microsofttestingplatform-services). The API consists of: ```cs From 81754a45813f8e6fb135c7121eefc0f42b7d8521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jul 2024 11:04:48 +0200 Subject: [PATCH 03/19] Fix nits --- .../unit-testing-platform-architecture-capabilities.md | 4 ++-- .../testing/unit-testing-platform-architecture-extensions.md | 4 ++-- .../testing/unit-testing-platform-architecture-services.md | 4 ++-- docs/core/testing/unit-testing-platform-architecture.md | 5 ++++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-capabilities.md b/docs/core/testing/unit-testing-platform-architecture-capabilities.md index 4ac739504caf6..10fa1b888a11d 100644 --- a/docs/core/testing/unit-testing-platform-architecture-capabilities.md +++ b/docs/core/testing/unit-testing-platform-architecture-capabilities.md @@ -1,6 +1,6 @@ --- -title: Microsoft.Testing.Platform architecture overview -description: Learn about how to extend Microsoft.Testing.Platform. +title: Microsoft.Testing.Platform capabilities overview +description: Learn about Microsoft.Testing.Platform capabilities concept. author: MarcoRossignoli ms.author: mrossignoli ms.date: 07/11/2024 diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index e64251067f34b..4ff7caa1c250a 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -1,12 +1,12 @@ --- -title: Microsoft.Testing.Platform architecture overview +title: Microsoft.Testing.Platform extensions architecture overview description: Learn about how to extend Microsoft.Testing.Platform. author: MarcoRossignoli ms.author: mrossignoli ms.date: 07/11/2024 --- -# Microsoft.Testing.Platform extensions +# Microsoft.Testing.Platform extensibility The testing platform consists of a [testing framework](#test-framework-extension) and any number of [extensions](#other-extensibility-points) that can operate *in-process* or *out-of-process*. diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index ca97a44b1f8cb..9847dd059768a 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -1,6 +1,6 @@ --- -title: Microsoft.Testing.Platform architecture overview -description: Learn about how to extend Microsoft.Testing.Platform. +title: Microsoft.Testing.Platform services overview +description: Learn about the Microsoft.Testing.Platform available services. author: MarcoRossignoli ms.author: mrossignoli ms.date: 07/11/2024 diff --git a/docs/core/testing/unit-testing-platform-architecture.md b/docs/core/testing/unit-testing-platform-architecture.md index 6a036f68946b7..30084e932579f 100644 --- a/docs/core/testing/unit-testing-platform-architecture.md +++ b/docs/core/testing/unit-testing-platform-architecture.md @@ -1,6 +1,6 @@ --- title: Microsoft.Testing.Platform architecture overview -description: Learn about how to extend Microsoft.Testing.Platform. +description: Learn about Microsoft.Testing.Platform architecture. author: MarcoRossignoli ms.author: mrossignoli ms.date: 07/11/2024 @@ -10,6 +10,9 @@ ms.date: 07/11/2024 Welcome to our new test platform! To help you get acquainted with its capabilities, we'll start with a simple example that demonstrates how to register and run a test. This foundational example will give you a solid understanding of the core functionality and how to get started quickly. +> [!NOTE] +> You can use [this code](https://github.com/microsoft/testfx/tree/main/samples/public/TestingPlatformExamples) as a coded example of all the concepts that will be covered in this article. + [Step 1: Register and Run a simple test application](#step-1-register-and-run-a-simple-test-application) In this initial example, we will walk you through the basic steps to declare and run a test application. This straightforward approach ensures that you can immediately start using the platform with minimal setup. From 0ac5f3e030aea9f7b3bfd91d869ddac823eb16ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jul 2024 11:06:50 +0200 Subject: [PATCH 04/19] More link fixing --- .../unit-testing-platform-architecture-extensions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index 4ff7caa1c250a..96fc5c459adaf 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -1059,7 +1059,7 @@ Let's describe the api: `OnTestHostProcessStartedAsync`: This method is invoked immediately after the test host starts. This method offers an object that implements the `ITestHostProcessInformation` interface, which provides key details about the test host process result. > [!IMPORTANT] -> The invocation of this method does not halt the test host's execution. If you need to pause it, you should register an [*in-process*](#microsofttestingplatform-extensions) extension such as [`ITestApplicationLifecycleCallbacks`](#the-itestapplicationlifecyclecallbacks-extensions) and synchronize it with the *out-of-process* extension. +> The invocation of this method does not halt the test host's execution. If you need to pause it, you should register an [*in-process*](#microsofttestingplatform-extensibility) extension such as [`ITestApplicationLifecycleCallbacks`](#the-itestapplicationlifecyclecallbacks-extensions) and synchronize it with the *out-of-process* extension. `OnTestHostProcessExitedAsync`: This method is invoked when the test suite execution is complete. This method supplies an object that adheres to the `ITestHostProcessInformation` interface, which conveys crucial details about the outcome of the test host process. @@ -1071,7 +1071,7 @@ The `ITestHostProcessInformation` interface provides the following details: ## Extensions execution order -The testing platform consists of a [testing framework](#test-framework-extension) and any number of extensions that can operate [*in-process*](#microsofttestingplatform-extensions) or [*out-of-process*](#microsofttestingplatform-extensions). This document outlines the **sequence of calls** to all potential extensibility points to provide clarity on when a feature is anticipated to be invoked. +The testing platform consists of a [testing framework](#test-framework-extension) and any number of extensions that can operate [*in-process*](#microsofttestingplatform-extensibility) or [*out-of-process*](#microsofttestingplatform-extensibility). This document outlines the **sequence of calls** to all potential extensibility points to provide clarity on when a feature is anticipated to be invoked. While a *sequence* could be used to depict this, we opt for a straightforward order of invocation calls, which allows for a more comprehensive commentary on the workflow. @@ -1122,7 +1122,7 @@ public interface IAsyncCleanableExtension ### The CompositeExtensionFactory -As outlined in the [extensions](#microsofttestingplatform-extensions) section, the testing platform enables you to implement interfaces to incorporate custom extensions both in and out of process. +As outlined in the [extensions](#microsofttestingplatform-extensibility) section, the testing platform enables you to implement interfaces to incorporate custom extensions both in and out of process. Each interface addresses a particular feature, and according to .NET design, you implement this interface in a specific object. You can register the extension itself using the specific registration API `AddXXX` from the `TestHost` or `TestHostController` object from the `ITestApplicationBuilder` as detailed in the corresponding sections. From 221f9ac8de3a9614b8dce537024427896c827c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 16 Jul 2024 15:14:33 +0200 Subject: [PATCH 05/19] Apply suggestions from code review Co-authored-by: David Pine --- ...ting-platform-architecture-capabilities.md | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-capabilities.md b/docs/core/testing/unit-testing-platform-architecture-capabilities.md index 10fa1b888a11d..e519510fcdb3c 100644 --- a/docs/core/testing/unit-testing-platform-architecture-capabilities.md +++ b/docs/core/testing/unit-testing-platform-architecture-capabilities.md @@ -8,27 +8,29 @@ ms.date: 07/11/2024 # Microsoft.Testing.Platform capabilities -In the context of the testing platform, a *capability* refers to the *potential to perform a specific action or provide specific information*. It is a means for the testing framework and extensions to *declare* their *ability* to *operate* in a certain manner or provide specific information to the *requesters*. +In the context of the testing platform, a *capability* refers to the *potential to perform a specific action or provide specific information*. It's a means for the testing framework and extensions to *declare* their *ability* to *operate* in a certain manner or provide specific information to the *requesters*. -The *requesters* can be any component involved in a test session, such as the platform itself, an extension, or the testing framework itself. +The *requesters* can be any component involved in a test session, such as the platform, an extension, or the testing framework itself. The primary objective of the capability system is to facilitate effective communication among the components involved in a test session, enabling them to exchange information and meet their respective needs accurately. ## Guided example -Let's consider a hypothetical example to demonstrate the necessity of a capability system. **Please note that this example is purely for illustrative purposes and is not currently implemented within the testing platform or any testing framework.** +Let's consider a hypothetical example to demonstrate the necessity of a capability system. -Imagine a situation where we have an extension that requires the testing framework to execute no more than one test at a time. Furthermore, after each test, the extension needs to know the CPU usage for that specific test. +> [!NOTE] +> This example is purely for illustrative purposes and isn't currently implemented within the testing platform or any testing framework. -To accommodate the above scenario, we need to inquire from the testing framework if: +Imagine a situation where you have an extension that requires the testing framework to execute no more than one test at a time. Furthermore, after each test, the extension needs to know the CPU usage for that specific test. + +To accommodate the preceding scenario, you need to inquire from the testing framework if: 1. It has the capability to execute only one test at a time. 2. It can provide information regarding the amount of CPU consumed by each test. -How can the extension determine if the testing framework has the ability to operate in this mode and provide CPU usage information for a test session? -In the testing platform, this capability is represented by an implementation of an interface that inherits from a base one named `Microsoft.Testing.Platform.Capabilities.ICapability`: +How can the extension determine if the testing framework has the ability to operate in this mode and provide CPU usage information for a test session? In the testing platform, this capability is represented by an implementation the `Microsoft.Testing.Platform.Capabilities.ICapability` interface: -```cs +```csharp // Base capabilities contracts public interface ICapability @@ -52,75 +54,73 @@ public interface ITestFrameworkCapability : ICapability } ``` -As you can see, the interface `ICapability` is *empty* because it can represent *any capability*, and the actual implementation will be context-dependent. You'll also observe the `ITestFrameworkCapability`, which inherits from `ICapability` to classify the capability. The capability system's generic nature allows for convenient grouping by context. The `ITestFrameworkCapability` groups all the capabilities implemented by the [testing framework](./unit-testing-platform-architecture-extensions.md#creating-a-testing-framework). The `ICapabilities` interface reveals the *set* of all capabilities implemented by an extension. Similarly, for the base one, we have a context-specific testing framework called `ITestFrameworkCapabilities`. The `ITestFrameworkCapabilities` is provided to the platform during the [testing framework registration](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework) process. +As you can see, the `ICapability` interface is a *marker* interface because it can represent *any capability*, and the actual implementation will be context dependent. You'll also observe the `ITestFrameworkCapability`, which inherits from `ICapability` to classify the capability. The capability system's generic nature allows for convenient grouping by context. The `ITestFrameworkCapability` groups all the capabilities implemented by the [testing framework](./unit-testing-platform-architecture-extensions.md#creating-a-testing-framework). The `ICapabilities` interface reveals the *set* of all capabilities implemented by an extension. Similarly, for the base one, there's a context-specific testing framework called `ITestFrameworkCapabilities`. The `ITestFrameworkCapabilities` is provided to the platform during the [testing framework registration](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework) process. -Now, let's attempt to create our capability to address the aforementioned scenario. We could define it as follows: +To create a capability that addresses the aforementioned scenario, you define it as follows: -```cs +```csharp public interface IDisableParallelismCapability : ITestFrameworkCapability { - bool CanDisableParallelism {get;} - bool CanProvidePerTestCPUConsumption {get;} + bool CanDisableParallelism { get; } + bool CanProvidePerTestCpuConsumption { get; } void Enable(); } ``` -If the testing framework implements this interface and we can query it at runtime, we can: +If the testing framework implements this interface, at runtime, the following can be queried: -* Verify if the testing framework has the ability to turn off parallelism `CanDisableParallelism = true` -* Determine if the testing framework can supply CPU usage data `CanProvidePerTestCPUConsumption = true` -* Request the testing adapter to activate this mode by invoking the `Enable()` method before the test session commences +* Verify if the testing framework has the ability to turn off parallelism `CanDisableParallelism = true`. +* Determine if the testing framework can supply CPU usage data `CanProvidePerTestCPUConsumption = true`. +* Request the testing adapter to activate this mode by invoking the `Enable()` method before the test session commences. The hypothetical code fragment inside the extension could be something like: -```cs -IServiceProvider serviceProvider = ...get service provider... -ITestFrameworkCapabilities testFrameworkCapabilities = serviceProvider.GetRequiredService(); +```csharp +IServiceProvider provider = null; // TODO: Get IServiceProvider. +ITestFrameworkCapabilities capabilities = serviceProvider.GetRequiredService(); -// We utilize the handy `GetCapability` API to search for the specific capability we wish to query. -IDisableParallelismCapability? capability = testFrameworkCapabilities.GetCapability(); +// Utilize the `GetCapability` API to search for the specific capability to query. +IDisableParallelismCapability? capability = capabilities.GetCapability(); if (capability is null) { - ...capability not supported... + // Capability not supported... } else { capability.Enable(); if (capability.CanDisableParallelism) { - ...do something... + // Do something... } - if (capability.CanProvidePerTestCPUConsumption) + if (capability.CanProvidePerTestCpuConsumption) { - ...do something... + // Do something... } } ``` -The example above illustrates how the capability infrastructure enables a powerful mechanism for communicating abilities between the components involved in a test session. While the sample demonstrates a capability specifically designed for the testing framework, any component can expose and implement extensions that inherit from `ICapability`. +The preceding example illustrates how the capability infrastructure enables a powerful mechanism for communicating abilities between the components involved in a test session. While the sample demonstrates a capability specifically designed for the testing framework, any component can expose and implement extensions that inherit from `ICapability`. -It's evident that not all details can be communicated through an interface. For example, in the scenario above, what should the extension expected if the `CanProvidePerTestCPUConsumption` is supported? What kind of custom information is expected to be transmitted via the [`IMessageBus`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) by the testing framework? The solution to this is **DOCUMENTATION OF THE CAPABILITY**. It's the responsibility of the capability *owner* to design, ship, and document it as clearly as possible to assist implementors who want to effectively *collaborate* with the extension that requires the specific capability. +It's evident that not all details can be communicated through an interface. Considering the previous example, what should the extension expect if the `CanProvidePerTestCpuConsumption` is supported? What kind of custom information is expected to be transmitted via the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) by the testing framework? The solution is **documentation of the capability**. It's the responsibility of the capability *owner* to design, ship, and document it as clearly as possible to assist implementors who want to effectively *collaborate* with the extension that requires the specific capability. -For instance, at the time of writing, the TRX report extension enables the testing framework to implement the necessary capability to accurately generate a TRX report. The extension to register is included in the package , but the capability to implement is found in the *contract only* package . +For instance, the TRX report extension enables the testing framework to implement the necessary capability to accurately generate a TRX report. The extension to register is included in the NuGet package, but the capability to implement is found in the *contract only* NuGet package. In conclusion, let's summarize the primary aspects of the capability system: -* It is essential for facilitating clear and stable communication between components. +* It's essential for facilitating clear and stable communication between components. * All capabilities should inherit from `ICapability` or an interface that inherits from it, and are exposed through a collection with the `ICapabilities` interface. -* It aids in the evolution of features without causing breaking changes. If a certain capability is not supported, appropriate action can be taken. +* It aids in the evolution of features without causing breaking changes. If a certain capability isn't supported, appropriate action can be taken. * The responsibility of designing, shipping, and documenting the usage of a capability lies with the *capability owner*. The testing platform can also *own* a capability in the same way as any other extension. -## Platform capabilities - ## Framework capabilities The platform exposes a specialized interface named `ITestFrameworkCapability` that is the base of all capabilities exposed for test frameworks. These capabilities are provided when [registering the test framework to the platform](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework). ### `IBannerMessageOwnerCapability` -An optional [test framework capability](#framework-capabilities) that allows the test framework to provide the banner message to the platform. If the message is null or if the capability is not present, the platform will use its default banner message. +An optional [test framework capability](#framework-capabilities) that allows the test framework to provide the banner message to the platform. If the message is `null` or if the capability is n't present, the platform will use its default banner message. -This capability implementation allows to abstract away the various conditions that the test framework may need to consider to decide whether or not the banner message should be displayed. +This capability implementation allows you to abstract away the various conditions that the test framework may need to consider when deciding whether or not the banner message should be displayed. The platform exposes the [`IPlatformInformation` service](./unit-testing-platform-architecture-services.md#the-iplatforminformation-service) to provide some information about the platform that could be useful when building your custom banner message. From 3af7e822140888740495eb9cc3e9761cb9a7030e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 16 Jul 2024 15:18:13 +0200 Subject: [PATCH 06/19] Apply suggestions from code review Co-authored-by: David Pine --- ...esting-platform-architecture-extensions.md | 106 +++++++++--------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index 96fc5c459adaf..9a502d4518036 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -10,50 +10,50 @@ ms.date: 07/11/2024 The testing platform consists of a [testing framework](#test-framework-extension) and any number of [extensions](#other-extensibility-points) that can operate *in-process* or *out-of-process*. -As outlined in the [architecture](./unit-testing-platform-architecture.md) section, the testing platform is designed to accommodate a variety of scenarios and extensibility points. The primary and essential extension is undoubtedly the [testing framework](#test-framework-extension) that our tests will utilize. Failing to register it will result in a startup error. **The [testing framework](#test-framework-extension) is the sole mandatory extension required to execute a testing session.** +As outlined in the [architecture](./unit-testing-platform-architecture.md) section, the testing platform is designed to accommodate a variety of scenarios and extensibility points. The primary and essential extension is undoubtedly the [testing framework](#test-framework-extension) that your tests will utilize. Failing to register this results in startup error. **The [testing framework](#test-framework-extension) is the sole mandatory extension required to execute a testing session.** -To support scenarios such as generating test reports, code coverage, retrying failed tests, and other potential features, we need to provide a mechanism that allows other extensions to work in conjunction with the [testing framework](#test-framework-extension) to deliver these features not inherently provided by the [testing framework](#test-framework-extension) itself. +To support scenarios such as generating test reports, code coverage, retrying failed tests, and other potential features, you need to provide a mechanism that allows other extensions to work in conjunction with the [testing framework](#test-framework-extension) to deliver these features not inherently provided by the [testing framework](#test-framework-extension) itself. -In essence, the [testing framework](#test-framework-extension) is the primary extension that supplies information about each test that makes up our test suite. It reports whether a specific test has succeeded, failed, skipped, etc., and can provide additional information about each test, such as a human-readable name (referred to as the display name), the source file, and the line where our test begins, among other things. +In essence, the [testing framework](#test-framework-extension) is the primary extension that supplies information about each test that makes up the test suite. It reports whether a specific test has succeeded, failed, skipped, and can provide additional information about each test, such as a human-readable name (referred to as the display name), the source file, and the line where our test begins, among other things. -The extensibility point allows us to utilize the information provided by the [testing framework](#test-framework-extension) to generate new artifacts or enhance existing ones with additional features. A commonly used extension is the TRX report generator, which subscribes to the [TestNodeUpdateMessage](#the-testnodeupdatemessage-data) and generates an XML report file from it. +The extensibility point enables the utilization of information provided by the [testing framework](#test-framework-extension) to generate new artifacts or to enhance existing ones with additional features. A commonly used extension is the TRX report generator, which subscribes to the [TestNodeUpdateMessage](#the-testnodeupdatemessage-data) and generates an XML report file from it. As discussed in the [architecture](./unit-testing-platform-architecture.md), there are certain extension points that *cannot* operate within the same process as the [testing framework](#test-framework-extension). The reasons typically include: * The need to modify the *environment variables* of the *test host*. Acting within the test host process itself is *too late*. -* The requirement to *monitor* the process from the outside because the *test host*, where our tests and user code run, might have some *user code bugs* that render the process itself *unstable*, leading to potential *hangs* or *crashes*. In such cases, the extension would crash or hang along with the *test host* process. +* The requirement to *monitor* the process from the outside because the *test host*, where tests and user code run, might have some *user code bugs* that render the process itself *unstable*, leading to potential *hangs* or *crashes*. In such cases, the extension would crash or hang along with the *test host* process. -Due to the reasons mentioned above, the extension points are categorized into two types: +Due to these reasons, the extension points are categorized into two types: -* *In-process extensions*: These extensions operate within the same process as the [testing framework](#test-framework-extension). +1. *In-process extensions*: These extensions operate within the same process as the [testing framework](#test-framework-extension). -You can register *in-process extensions* via the `ITestApplicationBuilder.TestHost` property: + You can register *in-process extensions* via the `ITestApplicationBuilder.TestHost` property: -```cs -... -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -testApplicationBuilder.TestHost.AddXXX(...); -... -``` + ```csharp + // ... + ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + builder.TestHost.AddXXX(...); + // ... + ``` -* *Out-of-process extensions*: These extensions function in a separate process, allowing them to monitor the test host without being influenced by the test host itself. +1. *Out-of-process extensions*: These extensions function in a separate process, allowing them to monitor the test host without being influenced by the test host itself. -You can register *out-of-process extensions* via the `ITestApplicationBuilder.TestHostControllers`. + You can register *out-of-process extensions* via the `ITestApplicationBuilder.TestHostControllers`. -```cs -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -testApplicationBuilder.TestHostControllers.AddXXX(...); -``` + ```csharp + ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + builder.TestHostControllers.AddXXX(...); + ``` -Lastly, some extensions are designed to function in both scenarios. These common extensions behave identically in both *hosts*. You can register these extensions either through the *TestHost* and *TestHostController* interfaces or directly at the `ITestApplicationBuilder` level. An example of such an extension is the [ICommandLineOptionsProvider](#the-icommandlineoptionsprovider-extensions). + Lastly, some extensions are designed to function in both scenarios. These common extensions behave identically in both *hosts*. You can register these extensions either through the *TestHost* and *TestHostController* interfaces or directly at the `ITestApplicationBuilder` level. An example of such an extension is the [ICommandLineOptionsProvider](#the-icommandlineoptionsprovider-extensions). ## The `IExtension` interface -The `IExtension` interface serves as the foundational interface for all extensibility points within the testing platform. It is primarily used to obtain descriptive information about the extension and, most importantly, to enable or disable the extension itself. +The `IExtension` interface serves as the foundational interface for all extensibility points within the testing platform. It's primarily used to obtain descriptive information about the extension and, most importantly, to enable or disable the extension itself. -Let's delve into the specifics: +Consider the following `IExension` interface: -```cs +```csharp public interface IExtension { string Uid { get; } @@ -64,27 +64,25 @@ public interface IExtension } ``` -`Uid`: This is the unique identifier for the extension. It's crucial to choose a unique value for this string to avoid conflicts with other extensions. +* `Uid`: Represents the unique identifier for the extension. It's crucial to choose a unique value for this string to avoid conflicts with other extensions. -`Version`: This represents the version of the interface. It MUST use [**semantic versioning**](https://semver.org/). +* `Version`: Represents the version of the interface. Requires [**semantic versioning**](https://semver.org/). -`DisplayName`: This is a user-friendly name that will appear in logs and when you request information using the `--info` command line option. +* `DisplayName`: A user-friendly name representation that will appear in logs and when you request information using the `--info` command line option. -`Description`: The description of the extension, will appear when you request information using the `--info` command line option. +* `Description`: The description of the extension, that appears when you request information using the `--info` command line option. -`IsEnabledAsync()`: This method is invoked by the testing platform when the extension is being instantiated. If the method returns false, the extension will be excluded. -This method typically makes decisions based on the [configuration file](./unit-testing-platform-architecture-services.md#the-iconfiguration-service) file or some [custom command line options](./unit-testing-platform-architecture-services.md#the-icommandlineoptions-service). Users often specify `--customExtensionOption` in the command line to opt into the extension itself. +* `IsEnabledAsync()`: This method is invoked by the testing platform when the extension is being instantiated. If the method returns `false`, the extension will be excluded. This method typically makes decisions based on the [configuration file](./unit-testing-platform-architecture-services.md#the-iconfiguration-service) or some [custom command line options](./unit-testing-platform-architecture-services.md#the-icommandlineoptions-service). Users often specify `--customExtensionOption` in the command line to opt into the extension itself. ## Test Framework extension -### Registering a testing framework +### Register a testing framework -This section explains how to register the test framework to the testing platform. -You can register only one testing framework per test application builder using the api `TestApplication.RegisterTestFramework` as shown [here](./unit-testing-platform-architecture.md) +This section explains how to register the test framework with the testing platform. You register only one testing framework per test application builder using the `TestApplication.RegisterTestFramework` API as shown in [the testing platform architecture](./unit-testing-platform-architecture.md) documentation. -The API's signature is as follows: +The registration API is defined as follows: -```cs +```csharp ITestApplicationBuilder RegisterTestFramework( Func capabilitiesFactory, Func adapterFactory); @@ -96,15 +94,13 @@ The `RegisterTestFramework` API expects two factories: The [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) interface is used to announce the capabilities supported by the testing framework to the platform and extensions. It allows the platform and extensions to interact correctly by implementing and supporting specific behaviors. For a better understanding of the [concept of capabilities](./unit-testing-platform-architecture-capabilities.md), refer to the respective section. -2. `Func`: This is a lambda function that takes in an [ITestFrameworkCapabilities](./unit-testing-platform-architecture-capabilities.md) object, which is the instance returned by the `Func`, and an [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to provide access to platform services once more. The expected return object is one that implements the [ITestFramework](#test-framework-extension) interface. The ITestFramework serves as the execution engine that discovers and runs tests, and communicates the results back to the testing platform. +1. `Func`: This is a delegate that takes in an [ITestFrameworkCapabilities](./unit-testing-platform-architecture-capabilities.md) object, which is the instance returned by the `Func`, and an [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to provide access to platform services once more. The expected return object is one that implements the [ITestFramework](#test-framework-extension) interface. The `ITestFramework` serves as the execution engine that discovers and runs tests, and then communicates the results back to the testing platform. The need for the platform to separate the creation of the [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) and the creation of the [ITestFramework](#test-framework-extension) is an optimization to avoid creating the test framework if the supported capabilities are not sufficient to execute the current testing session. -Below a sample of a test framework registration that returns empty capabilities. - -User code: +Consider the following user code example, which demonstrates a test framework registration that returns an empty capability set: -```cs +```csharp internal class TestingFrameworkCapabilities : ITestFrameworkCapabilities { public IReadOnlyCollection Capabilities => []; @@ -114,9 +110,9 @@ internal class TestingFramework : ITestFramework { public TestingFramework(ITestFrameworkCapabilities capabilities, IServiceProvider serviceProvider) { - ... + // ... } - ... + // Omitted for brevity... } public static class TestingFrameworkExtensions @@ -132,9 +128,9 @@ public static class TestingFrameworkExtensions ... ``` -Entry point with the registration: +Now, consider the corresponding entry point of this example with the registration code: -```cs +```csharp var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); // Register the testing framework testApplicationBuilder.AddTestingFramework(); @@ -143,9 +139,9 @@ return await testApplication.RunAsync(); ``` > [!NOTE] -> Returning empty [ITestFrameworkCapabilities](./unit-testing-platform-architecture-capabilities.md) should not prevent the execution of the test session. All test frameworks should be capable of discovering and running tests. The impact should be limited to extensions that may opt out if the test framework lacks a certain feature. +> Returning empty [ITestFrameworkCapabilities](./unit-testing-platform-architecture-capabilities.md) shouldn't prevent the execution of the test session. All test frameworks should be capable of discovering and running tests. The impact should be limited to extensions that may opt-out if the test framework lacks a certain feature. -### Creating a testing framework +### Create a testing framework The `Microsoft.Testing.Platform.Extensions.TestFramework.ITestFramework` is implemented by extensions that provide a test framework: @@ -158,11 +154,11 @@ public interface ITestFramework : IExtension } ``` -The `ITestFramework` interface inherits from the [IExtension](#the-iextension-interface) interface, which is an interface that all extension points inherit from. `IExtension` is used to retrieve the name and description of the extension. The `IExtension` also provides a way to dynamically enable or disable the extension in setup, through `Task IsEnabledAsync()`, please make sure that you return `true` from this method if you have no special needs. +The `ITestFramework` interface inherits from the [IExtension](#the-iextension-interface) interface, which is an interface that all extension points inherit from. `IExtension` is used to retrieve the name and description of the extension. The `IExtension` also provides a way to dynamically enable or disable the extension in setup, through `Task IsEnabledAsync()`. Please make sure that you return `true` from this method if you have no special needs. -#### CreateTestSessionAsync +#### The `CreateTestSessionAsync` method -The `CreateTestSessionAsync` is called at the start of the test session and is used to initialize the test framework. As we can see the api accepts a `CloseTestSessionContext` object and returns a `CloseTestSessionResult`. +The `CreateTestSessionAsync` method is called at the start of the test session and is used to initialize the test framework. The API accepts a `CloseTestSessionContext` object and returns a `CloseTestSessionResult`. ```cs public sealed class CreateTestSessionContext : TestSessionContext @@ -199,15 +195,15 @@ public sealed class CreateTestSessionResult } ``` -The `IsSuccess` can be used to indicate whether the session creation was successful. If it returns false, the test execution will be halted. +The `IsSuccess` property is used to indicate whether the session creation was successful. When it returns `false`, the test execution is halted. -#### CloseTestSessionAsync +#### The `CloseTestSessionAsync` method -The `CloseTestSessionAsync` mirrors the `CreateTestSessionAsync` in functionality, with the only difference being the object names. For more details, refer to the `CreateTestSessionAsync` section. +The `CloseTestSessionAsync` method is juxtaposed to the `CreateTestSessionAsync` in functionality, with the only difference being the object names. For more information, see the `CreateTestSessionAsync` section. -#### ExecuteRequestAsync +#### The `ExecuteRequestAsync` method -The `ExecuteRequestAsync` accepts an object of type `ExecuteRequestContext`. This object, as suggested by its name, holds the specifics about the action that the test framework is expected to perform. +The `ExecuteRequestAsync` method accepts an object of type `ExecuteRequestContext`. This object, as suggested by its name, holds the specifics about the action that the test framework is expected to perform. The `ExecuteRequestContext` definition is: ```cs @@ -243,7 +239,7 @@ sequenceDiagram ITestFramework-->>Testing platform: CloseTestSessionResult ``` -The diagram above illustrates that the testing platform issues 3 requests after creating the test framework instance. The test framework processes these requests and utilizes the `IMessageBus` service, which is included in the request itself, to deliver the result for each specific request. Once a particular request has been handled, the test framework invokes the `Complete()` method on it, indicating to the testing platform that the request has been fulfilled. +The preceding diagram illustrates that the testing platform issues three requests after creating the test framework instance. The test framework processes these requests and utilizes the `IMessageBus` service, which is included in the request itself, to deliver the result for each specific request. Once a particular request has been handled, the test framework invokes the `Complete()` method on it, indicating to the testing platform that the request has been fulfilled. The testing platform monitors all dispatched requests. Once all requests have been fulfilled, it invokes `CloseTestSessionAsync` and disposes of the instance (if `IDisposable/IAsyncDisposable` is implemented). It's evident that the requests and their completions can overlap, enabling concurrent and asynchronous execution of requests. > [!NOTE] From 067f3fec77662fe445000b1fe0fbb1e422175081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 16 Jul 2024 15:20:34 +0200 Subject: [PATCH 07/19] Use csharp code slugs instead of cs --- ...esting-platform-architecture-extensions.md | 88 +++++++++---------- ...-testing-platform-architecture-services.md | 32 +++---- .../unit-testing-platform-architecture.md | 10 +-- 3 files changed, 65 insertions(+), 65 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index 9a502d4518036..f38f48445e959 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -145,7 +145,7 @@ return await testApplication.RunAsync(); The `Microsoft.Testing.Platform.Extensions.TestFramework.ITestFramework` is implemented by extensions that provide a test framework: -```cs +```csharp public interface ITestFramework : IExtension { Task CreateTestSessionAsync(CreateTestSessionContext context); @@ -160,7 +160,7 @@ The `ITestFramework` interface inherits from the [IExtension](#the-iextension-in The `CreateTestSessionAsync` method is called at the start of the test session and is used to initialize the test framework. The API accepts a `CloseTestSessionContext` object and returns a `CloseTestSessionResult`. -```cs +```csharp public sealed class CreateTestSessionContext : TestSessionContext { public SessionUid SessionUid { get; } @@ -186,7 +186,7 @@ The `CancellationToken` is used to halt the execution of `CreateTestSessionAsync The return object is a `CloseTestSessionResult`: -```cs +```csharp public sealed class CreateTestSessionResult { public string? WarningMessage { get; set; } @@ -206,7 +206,7 @@ The `CloseTestSessionAsync` method is juxtaposed to the `CreateTestSessionAsync` The `ExecuteRequestAsync` method accepts an object of type `ExecuteRequestContext`. This object, as suggested by its name, holds the specifics about the action that the test framework is expected to perform. The `ExecuteRequestContext` definition is: -```cs +```csharp public sealed class ExecuteRequestContext { public IRequest Request { get; } @@ -271,7 +271,7 @@ Before proceeding to the next section, it's crucial to thoroughly comprehend the The `TestSessionContext` is a shared property across all requests, providing information about the ongoing test session: -```cs +```csharp public class TestSessionContext { public SessionUid SessionUid { get; } @@ -294,7 +294,7 @@ The `TestSessionContext` consists of the `SessionUid`, a unique identifier for t #### DiscoverTestExecutionRequest -```cs +```csharp public class DiscoverTestExecutionRequest { // Detailed in the custom section below @@ -309,7 +309,7 @@ The `DiscoverTestExecutionRequest` instructs the test framework **to discover** As outlined in the previous section, the property for a discovered test is `DiscoveredTestNodeStateProperty`. Here is a generic code snippet for reference: -```cs +```csharp ... var testNode = new TestNode() { @@ -324,7 +324,7 @@ await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(discoverTe #### RunTestExecutionRequest -```cs +```csharp public class RunTestExecutionRequest { // Detailed in the custom section below @@ -339,7 +339,7 @@ The `RunTestExecutionRequest` instructs the test framework **to execute** the te Here is a generic code snippet for reference: -```cs +```csharp ... var skippedTestNode = new TestNode() { @@ -383,7 +383,7 @@ await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(runTestExe As mentioned in the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) section, before utilizing the message bus, you must specify the type of data you intend to supply. The testing platform has defined a well-known type, `TestNodeUpdateMessage`, to represent the concept of a *test update information*. This part of the document will explain how to utilize this payload data. Let's examine the surface: -```cs +```csharp public sealed class TestNodeUpdateMessage(SessionUid sessionUid, TestNode testNode, TestNodeUid? parentTestNodeUid = null) { public TestNode TestNode { get; } @@ -439,7 +439,7 @@ The `PropertyBag` type is typically accessible in every `IData` and is utilized Finally this section makes clear that you test framework implementation needs to implement the `IDataProducer` that produces `TestNodeUpdateMessage`s like in the sample below: -```cs +```csharp internal sealed class TestingFramework : ITestFramework, IDataProducer { ... @@ -450,7 +450,7 @@ internal sealed class TestingFramework : ITestFramework, IDataProducer If your test adapter requires the publication of *files* during execution, you can find the recognized properties in this source file: . As you can see, you can provide file assets in a general manner or associate them with a specific `TestNode`. Remember, if you intend to push a `SessionFileArtifact`, you must declare it to the platform in advance, as shown below: -```cs +```csharp internal sealed class TestingFramework : ITestFramework, IDataProducer { ... @@ -482,13 +482,13 @@ Optional properties, on the other hand, enhance the testing experience by provid ##### Generic information -```cs +```csharp public record KeyValuePairStringProperty(string Key, string Value) : IProperty; ``` The `KeyValuePairStringProperty` stands for a general key/value pair data. -```cs +```csharp 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; @@ -497,7 +497,7 @@ public sealed record TestFileLocationProperty(string FilePath, LinePositionSpan `TestFileLocationProperty` is used to pinpoint the location of the test within the source file. This is particularly useful when the initiator is an IDE like Visual Studio or Visual Studio Code. -```cs +```csharp public sealed record TestMethodIdentifierProperty(string AssemblyFullName, string Namespace, string TypeName, string MethodName, string[] ParameterTypeFullNames, string ReturnTypeFullName) ``` @@ -506,7 +506,7 @@ public sealed record TestMethodIdentifierProperty(string AssemblyFullName, strin > [!NOTE] > The data needed to create this property can be conveniently obtained using the .NET reflection feature, using types from the `System.Reflection` namespace. -```cs +```csharp public sealed record TestMetadataProperty(string Key, string Value) ``` @@ -514,7 +514,7 @@ public sealed record TestMetadataProperty(string Key, string Value) ##### Discovery information -```cs +```csharp public sealed record DiscoveredTestNodeStateProperty(string? Explanation = null) { public static DiscoveredTestNodeStateProperty CachedInstance { get; } @@ -527,7 +527,7 @@ This property is **required**. ##### Execution information -```cs +```csharp public sealed record InProgressTestNodeStateProperty(string? Explanation = null) { public static InProgressTestNodeStateProperty CachedInstance { get; } @@ -537,7 +537,7 @@ public sealed record InProgressTestNodeStateProperty(string? Explanation = null) The `InProgressTestNodeStateProperty` informs the testing platform that the `TestNode` has been scheduled for execution and is currently in progress. Take note of the handy cached value offered by the `CachedInstance` property. -```cs +```csharp public readonly record struct TimingInfo(DateTimeOffset StartTime, DateTimeOffset EndTime, TimeSpan Duration); public sealed record StepTimingInfo(string Id, string Description, TimingInfo Timing); @@ -554,7 +554,7 @@ The `TimingProperty` is utilized to relay timing details about the `TestNode` ex ***One and only one*** of the following properties is **required** per `TestNode` and communicates the result of the `TestNode` to the testing platform. -```cs +```csharp public sealed record PassedTestNodeStateProperty(string? Explanation = null) { public static PassedTestNodeStateProperty CachedInstance { get; } @@ -564,7 +564,7 @@ public sealed record PassedTestNodeStateProperty(string? Explanation = null) `PassedTestNodeStateProperty` informs the testing platform that this `TestNode` is passed. Take note of the handy cached value offered by the `CachedInstance` property. -```cs +```csharp public sealed record SkippedTestNodeStateProperty(string? Explanation = null) { public static SkippedTestNodeStateProperty CachedInstance { get; } @@ -574,7 +574,7 @@ public sealed record SkippedTestNodeStateProperty(string? Explanation = null) `SkippedTestNodeStateProperty` informs the testing platform that this `TestNode` was skipped. Take note of the handy cached value offered by the `CachedInstance` property. -```cs +```csharp public sealed record FailedTestNodeStateProperty { public FailedTestNodeStateProperty(string explanation) @@ -585,7 +585,7 @@ public sealed record FailedTestNodeStateProperty `FailedTestNodeStateProperty` informs the testing platform that this `TestNode` is failed after an assertion. -```cs +```csharp public sealed record ErrorTestNodeStateProperty { public ErrorTestNodeStateProperty(string explanation) @@ -596,7 +596,7 @@ public sealed record ErrorTestNodeStateProperty `ErrorTestNodeStateProperty` informs the testing platform that this `TestNode` has failed. This type of failure is different from the `FailedTestNodeStateProperty`, which is used for assertion failures. For example, you can report issues like test initialization errors with `ErrorTestNodeStateProperty`. -```cs +```csharp public sealed record TimeoutTestNodeStateProperty { public TimeoutTestNodeStateProperty(string explanation) @@ -608,7 +608,7 @@ public sealed record TimeoutTestNodeStateProperty `TimeoutTestNodeStateProperty` informs the testing platform that this `TestNode` is failed for a timeout reason. You can report the timeout using the `Timeout` property. -```cs +```csharp public sealed record CancelledTestNodeStateProperty { public CancelledTestNodeStateProperty(string explanation) @@ -628,7 +628,7 @@ public sealed record CancelledTestNodeStateProperty As discussed in the [architecture](./unit-testing-platform-architecture.md) section, the initial step involves creating the `ITestApplicationBuilder` to register the testing framework and extensions with it. -```cs +```csharp ... var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); ... @@ -642,7 +642,7 @@ Arguments **must be prefixed** with a double dash `--`. For example, `--filter`. If a component such as a testing framework or an extension point wishes to offer custom command-line options, it can do so by implementing the `ICommandLineOptionsProvider` interface. This implementation can then be registered with the `ITestApplicationBuilder` via the registration factory of the `CommandLine` property, as shown: -```cs +```csharp ... testApplicationBuilder.CommandLine.AddProvider(() => new CustomCommandLineOptions()); ... @@ -650,7 +650,7 @@ testApplicationBuilder.CommandLine.AddProvider(() => new CustomCommandLineOption In the example provided, `CustomCommandLineOptions` is an implementation of the `ICommandLineOptionsProvider` interface, This interface comprises the following members and data types: -```cs +```csharp public interface ICommandLineOptionsProvider : IExtension { IReadOnlyCollection GetCommandLineOptions(); @@ -713,7 +713,7 @@ For examples, refer to the [System.CommandLine arity table](https://learn.micros For instance, if we have a parameter named `--dop` that represents the degree of parallelism for our custom testing framework, a user might input `--dop 0`. In this scenario, the value 0 would be invalid because we anticipate a degree of parallelism of 1 or more. By using `ValidateOptionArgumentsAsync`, we can perform upfront validation and return an error message if necessary. A possible implementation for the sample above could be: -```cs +```csharp public Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) { if (commandOption.Name == "dop") @@ -733,7 +733,7 @@ A possible implementation for the sample above could be: For example, let's say our testing framework has the capability to generate a test result report and save it to a file. This feature is accessed using the `--generatereport` option, and the filename is specified with `--reportfilename myfile.rep`. In this scenario, if a user only provides the `--generatereport` option without specifying a filename, the validation should fail because the report cannot be generated without a filename. A possible implementation for the sample above could be: -```cs +```csharp public Task ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) { bool generateReportEnabled = commandLineOptions.IsOptionSet(GenerateReportOption); @@ -753,7 +753,7 @@ The `ITestSessionLifeTimeHandler` is an *in-process* extension that enables the To register a custom `ITestSessionLifeTimeHandler`, utilize the following API: -```cs +```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); ... testApplicationBuilder.TestHost.AddTestSessionLifetimeHandle(serviceProvider => new CustomTestSessionLifeTimeHandler()); @@ -767,7 +767,7 @@ The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture The `ITestSessionLifeTimeHandler` interface includes the following methods: -```cs +```csharp public interface ITestSessionLifetimeHandler : ITestHostExtension { Task OnTestSessionStartingAsync(SessionUid sessionUid, CancellationToken cancellationToken); @@ -802,7 +802,7 @@ The `ITestApplicationLifecycleCallbacks` is an *in-process* extension that enabl To register a custom `ITestApplicationLifecycleCallbacks`, utilize the following api: -```cs +```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); ... testApplicationBuilder.TestHost.AddTestApplicationLifecycleCallbacks(serviceProvider @@ -817,7 +817,7 @@ The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture The `ITestApplicationLifecycleCallbacks` interface includes the following methods: -```cs +```csharp public interface ITestApplicationLifecycleCallbacks : ITestHostExtension { Task BeforeRunAsync(CancellationToken cancellationToken); @@ -847,7 +847,7 @@ The `IDataConsumer` is an *in-process* extension capable of subscribing to and r To register a custom `IDataConsumer`, utilize the following api: -```cs +```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); ... testApplicationBuilder.TestHost.AddDataConsumer(serviceProvider @@ -862,7 +862,7 @@ The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture The `IDataConsumer` interface includes the following methods: -```cs +```csharp public interface IDataConsumer : ITestHostExtension { Type[] DataTypesConsumed { get; } @@ -884,7 +884,7 @@ The `IDataConsumer` is a type of `ITestHostExtension`, which serves as a base fo A sample implementation of a consumer that wants to elaborate the [`TestNodeUpdateMessage`](#the-testnodeupdatemessage-data) produced by a [testing framework](#test-framework-extension) could be: -```cs +```csharp internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer { public Type[] DataTypesConsumed => new[] { typeof(TestNodeUpdateMessage) }; @@ -944,7 +944,7 @@ The `ITestHostEnvironmentVariableProvider` is an *out-of-process* extension that To register a custom `ITestHostEnvironmentVariableProvider`, utilize the following API: -```cs +```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); ... testApplicationBuilder.TestHostControllers.AddEnvironmentVariableProvider(serviceProvider => @@ -959,7 +959,7 @@ The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture The `ITestHostEnvironmentVariableProvider` interface includes the following methods and types: -```cs +```csharp public interface ITestHostEnvironmentVariableProvider : ITestHostControllersExtension, IExtension { Task UpdateAsync(IEnvironmentVariables environmentVariables); @@ -1016,7 +1016,7 @@ The `ITestHostProcessLifetimeHandler` is an *out-of-process* extension that allo To register a custom `ITestHostProcessLifetimeHandler`, utilize the following API: -```cs +```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); ... testApplicationBuilder.TestHostControllers.AddProcessLifetimeHandler(serviceProvider => @@ -1031,7 +1031,7 @@ The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture The `ITestHostProcessLifetimeHandler` interface includes the following methods: -```cs +```csharp public interface ITestHostProcessLifetimeHandler : ITestHostControllersExtension { Task BeforeTestHostProcessStartAsync(CancellationToken cancellationToken); @@ -1095,7 +1095,7 @@ The creation of the testing framework and extensions through factories adheres t Therefore, the testing platform provides a method to initialize an extension using the async/await pattern through a simple interface. For symmetry, it also offers an async interface for cleanup that extensions can implement seamlessly. -```cs +```csharp public interface IAsyncInitializableExtension { Task InitializeAsync(); @@ -1130,7 +1130,7 @@ For instance, consider a type that implements both `ITestSessionLifetimeHandler` What you should do is to normally implement the interfaces: -```cs +```csharp internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ... { ... @@ -1139,7 +1139,7 @@ internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ... Once you've created the `CompositeExtensionFactory` for your type, you can register it with both the `IDataConsumer` and `ITestSessionLifetimeHandler` APIs, which offer an overload for the `CompositeExtensionFactory`: -```cs +```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); ... CompositeExtensionFactory compositeExtensionFactory = new(serviceProvider => new CustomExtension()); diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index 9847dd059768a..db752b6b881c4 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -12,7 +12,7 @@ The testing platform offers valuable services to both the testing framework and The `IServiceProvider` is derived directly from the base class library. -```cs +```csharp namespace System { public interface IServiceProvider @@ -24,7 +24,7 @@ namespace System The testing platform offers handy extension methods to access well-known service objects. All these methods are housed in a static class within the `Microsoft.Testing.Platform.Services` namespace. -```cs +```csharp public static class ServiceProviderExtensions { public static TService GetRequiredService(this IServiceProvider provider) @@ -42,7 +42,7 @@ Most of the registration factories exposed by extension points, which can be reg For example, we encountered it earlier when discussing [registering the testing framework](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework). -```cs +```csharp ITestApplicationBuilder RegisterTestFramework( Func capabilitiesFactory, Func adapterFactory); @@ -61,7 +61,7 @@ The `IConfiguration` interface can be retrieved using the [`IServiceProvider`](# The interface is a straightforward key-value pair of strings: -```cs +```csharp public interface IConfiguration { string? this[string key] { get; } @@ -82,7 +82,7 @@ The JSON file follows a hierarchical structure. To access child properties, you The code snippet would look something like this: -```cs +```csharp IServiceProvider serviceProvider = ...get the service provider... IConfiguration configuration = serviceProvider.GetConfiguration(); if (configuration["CustomTestingFramework:DisableParallelism"] == bool.TrueString) @@ -106,7 +106,7 @@ In the case of an array, such as: The syntax to access to the fist element ("ThreadPool") is: -```cs +```csharp IServiceProvider serviceProvider = ...get the service provider... IConfiguration configuration = serviceProvider.GetConfiguration(); var fistElement = configuration["CustomTestingFramework:Engine:0"]; @@ -127,7 +127,7 @@ setx CustomTestingFramework__DisableParallelism=True You can choose not to use the environment variable configuration source when creating the `ITestApplicationBuilder`: -```cs +```csharp var testApplicationOptions = new TestApplicationOptions(); testApplicationOptions.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource = false; ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args, testApplicationOptions); @@ -137,7 +137,7 @@ ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBui The `ICommandLineOptions` service is utilized to fetch details regarding the command-line options that the platform has parsed. The APIs available include: -```cs +```csharp public interface ICommandLineOptions { bool IsOptionSet(string optionName); @@ -167,7 +167,7 @@ The options you can choose from include: From a coding standpoint, to log information, you need to obtain the `ILoggerFactory` from the [`IServiceProvider`](#microsofttestingplatform-services). The `ILoggerFactory` API is as follows: -```cs +```csharp public interface ILoggerFactory { ILogger CreateLogger(string categoryName); @@ -181,7 +181,7 @@ public static class LoggerFactoryExtensions The logger factory allows you to create an `ILogger` object using the `CreateLogger` API. There's also a convenient API that accepts a generic argument, which will be used as the category name. -```cs +```csharp public interface ILogger { Task LogAsync(LogLevel logLevel, TState state, Exception? exception, Func formatter); @@ -216,7 +216,7 @@ public static class LoggingExtensions The `ILogger` object, which is created by the `ILoggerFactory`, offers APIs for logging information at various levels. These logging levels include: -```cs +```csharp public enum LogLevel { Trace, @@ -231,7 +231,7 @@ public enum LogLevel Here's an example of how you might use the logging API: -```cs +```csharp ... IServiceProvider serviceProvider = ...get the service provider... ILoggerFactory loggerFactory = serviceProvider.GetLoggerFactory(); @@ -260,7 +260,7 @@ As illustrated in the diagram, which includes an extensions and a test framework The `IMessageBus` satisfied the *pushing action* to the bus and the api is: -```cs +```csharp public interface IMessageBus { Task PublishAsync(IDataProducer dataProducer, IData data); @@ -302,7 +302,7 @@ The most traditional example of an *output device* is the console output. To transmit data to the *output device*, you must obtain the `IOutputDevice` from the [`IServiceProvider`](#microsofttestingplatform-services). The API consists of: -```cs +```csharp public interface IOutputDevice { Task DisplayAsync(IOutputDeviceDataProducer producer, IOutputDeviceData data); @@ -322,7 +322,7 @@ The `IOutputDeviceData` serves as a placeholder interface. The concept behind `I The testing platform, by default, offers a traditional colored text model for the `IOutputDeviceData` object: -```cs +```csharp public class TextOutputDeviceData : IOutputDeviceData { public TextOutputDeviceData(string text) @@ -344,7 +344,7 @@ public sealed class SystemConsoleColor : IColor Here's an example of how you might use the colored text with the *active* output device: -```cs +```csharp IServiceProvider serviceProvider = ...get the service provider... IOutputDevice outputDevice = serviceProvider.GetOutputDevice(); await outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"TestingFramework version '{Version}' running tests with parallelism of {_dopValue}") { ForegroundColor = new SystemConsoleColor() { ConsoleColor = ConsoleColor.Green } }); diff --git a/docs/core/testing/unit-testing-platform-architecture.md b/docs/core/testing/unit-testing-platform-architecture.md index 30084e932579f..758ca1538f129 100644 --- a/docs/core/testing/unit-testing-platform-architecture.md +++ b/docs/core/testing/unit-testing-platform-architecture.md @@ -45,7 +45,7 @@ To introduce the architecture of the testing platform, we will use the classic c In a console project `Contoso.UnitTests.exe` the following `Main` method defines the entry point: -```cs +```csharp public static async Task Main(string[] args) { ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); @@ -89,7 +89,7 @@ The testing platform accommodates this by having **out-of-process** extensions. The following example demonstrates how to register a code coverage feature using a **TestHostController** extension. -```cs +```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); testApplicationBuilder.RegisterTestFramework(/* test framework registration factories */); testApplicationBuilder.AddCodeCoverage(); @@ -99,7 +99,7 @@ return await testApplication.RunAsync(); The `testApplicationBuilder.AddCodeCoverage();` internally uses the **TestHostController** extensibility point, which is an out-of-process extensibility point. -```cs +```csharp public static class TestApplicationBuilderExtensions { public static ITestApplicationBuilder AddCodeCoverage(this ITestApplicationBuilder testApplicationBuilder) @@ -131,7 +131,7 @@ The above section provides a brief introduction to the architecture of the testi 1. **In process** extensions can be accessed through the `TestHost` property of the test application builder. In process means that they will run in the same process as the test framework. - ```cs + ```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); testApplicationBuilder.RegisterTestFramework(/* test framework registration factories */); testApplicationBuilder.TestHost.AddXXX(...); @@ -141,7 +141,7 @@ The above section provides a brief introduction to the architecture of the testi 1. **Out of process** extensions can be accessed through the `TestHostControllers` property of the test application builder. These extensions run in a separate process from the test framework to "observe" it. - ```cs + ```csharp ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); testApplicationBuilder.TestHostControllers.AddXXX(...); ``` From 77c9830293450ef08576e4a3d0c077db926e8d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Wed, 17 Jul 2024 16:34:53 +0200 Subject: [PATCH 08/19] Apply suggestions from code review Co-authored-by: David Pine --- .../unit-testing-platform-architecture.md | 65 +++++++++++-------- .../testing/unit-testing-platform-intro.md | 4 +- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture.md b/docs/core/testing/unit-testing-platform-architecture.md index 758ca1538f129..3c625d7bf61f2 100644 --- a/docs/core/testing/unit-testing-platform-architecture.md +++ b/docs/core/testing/unit-testing-platform-architecture.md @@ -11,19 +11,19 @@ ms.date: 07/11/2024 Welcome to our new test platform! To help you get acquainted with its capabilities, we'll start with a simple example that demonstrates how to register and run a test. This foundational example will give you a solid understanding of the core functionality and how to get started quickly. > [!NOTE] -> You can use [this code](https://github.com/microsoft/testfx/tree/main/samples/public/TestingPlatformExamples) as a coded example of all the concepts that will be covered in this article. +> All of the concepts in this article are exemplified in the Microsoft Test Framework repository as a complete sample. For more information, see the [Sample code](https://github.com/microsoft/testfx/tree/main/samples/public/TestingPlatformExamples). [Step 1: Register and Run a simple test application](#step-1-register-and-run-a-simple-test-application) -In this initial example, we will walk you through the basic steps to declare and run a test application. This straightforward approach ensures that you can immediately start using the platform with minimal setup. +In this initial example, you walk through the basic steps to declare and run a test application. This straightforward approach ensures that you can immediately start using the platform with minimal setup. [Step 2: Extending the Platform](#step-2-extending-the-platform) -After you've discovered how to create your first test application, we will explore an example of extension to cover partially the concepts surrounding the test application extensions. +After you've discovered how to create your first test application, you explore an example of extension to cover partially the concepts surrounding the test application extensions. [Step 3: Comprehensive Overview of Extension Points](#step-3-comprehensive-overview-of-extension-points) -Once you're comfortable with the basics, we'll delve into the various extension points. This will include: +Once you're comfortable with the basics, you delve into the various extension points. This will include: 1. **Platform and Test Framework Capabilities**: Understanding the full range of capabilities provided by the platform and the test framework, allowing you to leverage them effectively. @@ -35,7 +35,7 @@ Once you're comfortable with the basics, we'll delve into the various extension [Step 4: Available Services and Helpers](#step-4-available-services) -Finally, we will provide an exhaustive list of the available services and helper functions within the platform. This section will serve as a reference to help you leverage all the tools at your disposal for creating robust and efficient test extensions. +Finally, you review an exhaustive list of the available services and helper functions within the platform. This section will serve as a reference to help you leverage all the tools at your disposal for creating robust and efficient test extensions. By following this structured approach, you will gain a comprehensive understanding of our test platform, from basic usage to advanced customization. Let's get started and explore the full potential of what our platform can offer! @@ -48,10 +48,13 @@ In a console project `Contoso.UnitTests.exe` the following `Main` method defines ```csharp public static async Task Main(string[] args) { - ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); - testApplicationBuilder.RegisterTestFramework(/* test framework registration factories */); - using ITestApplication testApplication = await testApplicationBuilder.BuildAsync(); - return await testApplication.RunAsync(); + ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + + builder.RegisterTestFramework(); + + using ITestApplication testApp = await builder.BuildAsync(); + + return await testApp.RunAsync(); } ``` @@ -83,30 +86,37 @@ Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 5ms - Contoso.Un ## Step 2: Extending the platform -Test runs commonly collect code coverage information, or similar information to evaluate code quality. Such workloads may require configuration to be done before the test host process starts, for example setting environment variables. +Test runs commonly collect code coverage information, or similar information to evaluate code quality. Such workloads may require configuration before the test host process starts, for example setting environment variables. The testing platform accommodates this by having **out-of-process** extensions. When running with an out-of-process extensions, the testing platform will start multiple processes and it will manage them appropriately. The following example demonstrates how to register a code coverage feature using a **TestHostController** extension. ```csharp -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -testApplicationBuilder.RegisterTestFramework(/* test framework registration factories */); -testApplicationBuilder.AddCodeCoverage(); -using ITestApplication testApplication = await testApplicationBuilder.BuildAsync(); -return await testApplication.RunAsync(); +ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + +builder.RegisterTestFramework(); +builder.AddCodeCoverage(); + +using ITestApplication testApp = await builder.BuildAsync(); + +return await testApp.RunAsync(); ``` -The `testApplicationBuilder.AddCodeCoverage();` internally uses the **TestHostController** extensibility point, which is an out-of-process extensibility point. +The `builder.AddCodeCoverage();` internally uses the `TestHostController` extensibility point, which is an out-of-process extensibility point. ```csharp public static class TestApplicationBuilderExtensions { - public static ITestApplicationBuilder AddCodeCoverage(this ITestApplicationBuilder testApplicationBuilder) + public static ITestApplicationBuilder AddCodeCoverage( + this ITestApplicationBuilder builder) { - testApplicationBuilder.TestHostControllers.AddEnvironmentVariableProvider(...); - .... - return testApplicationBuilder; + builder.TestHostControllers + .AddEnvironmentVariableProvider(/* ... */); + + // ... + + return builder; } } ``` @@ -132,9 +142,11 @@ The above section provides a brief introduction to the architecture of the testi 1. **In process** extensions can be accessed through the `TestHost` property of the test application builder. In process means that they will run in the same process as the test framework. ```csharp - ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); - testApplicationBuilder.RegisterTestFramework(/* test framework registration factories */); - testApplicationBuilder.TestHost.AddXXX(...); + ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + + builder.RegisterTestFramework(); + + builder.TestHost.AddXXX(/* ... */); ``` As observed, the most crucial extension point is the in-process *testing framework* (`RegisterTestFramework`), which is the only **mandatory** one. @@ -142,8 +154,9 @@ The above section provides a brief introduction to the architecture of the testi 1. **Out of process** extensions can be accessed through the `TestHostControllers` property of the test application builder. These extensions run in a separate process from the test framework to "observe" it. ```csharp - ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); - testApplicationBuilder.TestHostControllers.AddXXX(...); + ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + + builder.TestHostControllers.AddXXX(/* ... */); ``` ## Step 3: Comprehensive Overview of Extension points @@ -152,6 +165,6 @@ Let's start by getting familiar with the concept of [capabilities](./unit-testin ## Step 4: Available services -The testing platform offers valuable services to both the testing framework and extension points. These services cater to common needs such as accessing the configuration, parsing and retrieving command-line arguments, obtaining the logging factory, and accessing the logging system, among others. `IServiceProvider` implements the [service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern) for the testing platform. +The testing platform offers valuable services to both the testing framework and extension points. These services cater to common needs such as accessing the configuration, parsing and retrieving command-line arguments, obtaining the logging factory, and accessing the logging system, among others. `IServiceProvider` implements the _service locator pattern_ for the testing platform. All the services, helpers and technical information about how to access and use these services is listed [here](./unit-testing-platform-architecture-services.md). diff --git a/docs/core/testing/unit-testing-platform-intro.md b/docs/core/testing/unit-testing-platform-intro.md index df5ee2cd8c057..aae8fa5473c5f 100644 --- a/docs/core/testing/unit-testing-platform-intro.md +++ b/docs/core/testing/unit-testing-platform-intro.md @@ -16,7 +16,7 @@ Microsoft.Testing.Platform is a lightweight and portable alternative to [VSTest] This new testing platform is built on the .NET Developer Experience Testing team experience and aims at addressing the challenges encountered since the release of .NET Core in 2016. While there is a high level of compatibility between the .NET Framework and the .NET Core/.NET, some key features like the plugin system and the new possible form factors of .NET compilations have made it complex to evolve or fully support the new runtime feature with the current [VSTest platform](https://github.com/microsoft/vstest) architecture. -The main driving factors for the evolution of the new testing platform are: +The main driving factors for the evolution of the new testing platform are detailed in the following: * **Determinism**: Ensuring that running the same tests in different contexts (local, CI) will produce the same result. The new runtime does not rely on reflection or any other dynamic .NET runtime feature to coordinate a test run. @@ -24,7 +24,7 @@ The main driving factors for the evolution of the new testing platform are: * **Compile-time registration of extensions**: Extensions, such as test frameworks and in/out-of-process extensions, are registered during compile-time to ensure determinism and to facilitate detection of inconsistencies. -* **0 dependencies**: The core of the platform is a single .NET assembly, `Microsoft.Testing.Platform.dll`, which has no dependencies other than the supported runtimes. +* **Zero dependencies**: The core of the platform is a single .NET assembly, `Microsoft.Testing.Platform.dll`, which has no dependencies other than the supported runtimes. * **Hostable**: The test runtime can be hosted in any .NET application. While a console application is commonly used to run tests, you can create a test application in any type of .NET application. This allows you to run tests within special contexts, such as devices or browsers, where there may be limitations. From d81a5acdd3f65493979e9cf678d9dd46eee1cf5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 22 Jul 2024 16:37:58 +0200 Subject: [PATCH 09/19] Apply suggestions from code review Co-authored-by: David Pine --- ...esting-platform-architecture-extensions.md | 232 ++++++++++++------ 1 file changed, 155 insertions(+), 77 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index f38f48445e959..bf439c254aeff 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -125,7 +125,7 @@ public static class TestingFrameworkExtensions } } -... +// ... ``` Now, consider the corresponding entry point of this example with the registration code: @@ -242,6 +242,7 @@ sequenceDiagram The preceding diagram illustrates that the testing platform issues three requests after creating the test framework instance. The test framework processes these requests and utilizes the `IMessageBus` service, which is included in the request itself, to deliver the result for each specific request. Once a particular request has been handled, the test framework invokes the `Complete()` method on it, indicating to the testing platform that the request has been fulfilled. The testing platform monitors all dispatched requests. Once all requests have been fulfilled, it invokes `CloseTestSessionAsync` and disposes of the instance (if `IDisposable/IAsyncDisposable` is implemented). It's evident that the requests and their completions can overlap, enabling concurrent and asynchronous execution of requests. + > [!NOTE] > Currently, the testing platform does not send overlapping requests and waits for the completion of a request >> before sending the next one. However, this behavior may change in the future. The support for concurrent requests will be determined through the [capabilities](./unit-testing-platform-architecture-capabilities.md) system. @@ -256,6 +257,7 @@ For a comprehensive list of information that can be published to the testing pla `CancellationToken`: This token is utilized to interrupt the processing of a particular request. `Complete()`: As depicted in the previous sequence, the `Complete` method notifies the platform that the request has been successfully processed and all relevant information has been transmitted to the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service). + > [!WARNING] > Neglecting to invoke `Complete()` on the request will result in the test application becoming unresponsive. @@ -310,16 +312,21 @@ The `DiscoverTestExecutionRequest` instructs the test framework **to discover** As outlined in the previous section, the property for a discovered test is `DiscoveredTestNodeStateProperty`. Here is a generic code snippet for reference: ```csharp -... -var testNode = new TestNode() +var testNode = new TestNode { Uid = GenerateUniqueStableId(), DisplayName = GetDisplayName(), - Properties = new PropertyBag(DiscoveredTestNodeStateProperty.CachedInstance), + Properties = new PropertyBag( + DiscoveredTestNodeStateProperty.CachedInstance), }; -await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(discoverTestExecutionRequest.Session.SessionUid, testNode)); -... +await context.MessageBus.PublishAsync( + this, + new TestNodeUpdateMessage( + discoverTestExecutionRequest.Session.SessionUid, + testNode)); + +// ... ``` #### RunTestExecutionRequest @@ -340,51 +347,80 @@ The `RunTestExecutionRequest` instructs the test framework **to execute** the te Here is a generic code snippet for reference: ```csharp -... var skippedTestNode = new TestNode() { Uid = GenerateUniqueStableId(), DisplayName = GetDisplayName(), - Properties = new PropertyBag(SkippedTestNodeStateProperty.CachedInstance), + Properties = new PropertyBag( + SkippedTestNodeStateProperty.CachedInstance), }; -await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(runTestExecutionRequest.Session.SessionUid, skippedTestNode)); -... +await context.MessageBus.PublishAsync( + this, + new TestNodeUpdateMessage( + runTestExecutionRequest.Session.SessionUid, + skippedTestNode)); + +// ... + var successfulTestNode = new TestNode() { Uid = GenerateUniqueStableId(), DisplayName = GetDisplayName(), - Properties = new PropertyBag(PassedTestNodeStateProperty.CachedInstance), + Properties = new PropertyBag( + PassedTestNodeStateProperty.CachedInstance), }; -await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(runTestExecutionRequest.Session.SessionUid, successfulTestNode)); -... +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)), + Properties = new PropertyBag( + new FailedTestNodeStateProperty(assertionException)), }; -await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(runTestExecutionRequest.Session.SessionUid, assertionFailedTestNode)); -... +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!)), + Properties = new PropertyBag( + new ErrorTestNodeStateProperty(ex.InnerException!)), }; -await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(runTestExecutionRequest.Session.SessionUid, failedTestNode)); +await context.MessageBus.PublishAsync( + this, + new TestNodeUpdateMessage( + runTestExecutionRequest.Session.SessionUid, + failedTestNode)); ``` ### The `TestNodeUpdateMessage` data As mentioned in the [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) section, before utilizing the message bus, you must specify the type of data you intend to supply. The testing platform has defined a well-known type, `TestNodeUpdateMessage`, to represent the concept of a *test update information*. + This part of the document will explain how to utilize this payload data. Let's examine the surface: ```csharp -public sealed class TestNodeUpdateMessage(SessionUid sessionUid, TestNode testNode, TestNodeUid? parentTestNodeUid = null) +public sealed class TestNodeUpdateMessage( + SessionUid sessionUid, + TestNode testNode, + TestNodeUid? parentTestNodeUid = null) { public TestNode TestNode { get; } public TestNodeUid? ParentTestNodeUid { get; } @@ -440,22 +476,35 @@ The `PropertyBag` type is typically accessible in every `IData` and is utilized Finally this section makes clear that you test framework implementation needs to implement the `IDataProducer` that produces `TestNodeUpdateMessage`s like in the sample below: ```csharp -internal sealed class TestingFramework : ITestFramework, IDataProducer +internal sealed class TestingFramework + : ITestFramework, IDataProducer { - ... - public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage) }; - ... + // ... + + public Type[] DataTypesProduced => + [ + typeof(TestNodeUpdateMessage) + ]; + + // ... } ``` If your test adapter requires the publication of *files* during execution, you can find the recognized properties in this source file: . As you can see, you can provide file assets in a general manner or associate them with a specific `TestNode`. Remember, if you intend to push a `SessionFileArtifact`, you must declare it to the platform in advance, as shown below: ```csharp -internal sealed class TestingFramework : ITestFramework, IDataProducer +internal sealed class TestingFramework + : ITestFramework, IDataProducer { - ... - public Type[] DataTypesProduced => new[] { typeof(TestNodeUpdateMessage), typeof(SessionFileArtifact) }; - ... + // ... + + public Type[] DataTypesProduced => + [ + typeof(TestNodeUpdateMessage), + typeof(SessionFileArtifact) + ]; + + // ... } ``` @@ -483,22 +532,39 @@ Optional properties, on the other hand, enhance the testing experience by provid ##### Generic information ```csharp -public record KeyValuePairStringProperty(string Key, string Value) : IProperty; +public record KeyValuePairStringProperty( + string Key, string Value) : IProperty; ``` The `KeyValuePairStringProperty` stands for a general key/value pair data. ```csharp -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); +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` is used to pinpoint the location of the test within the source file. This is particularly useful when the initiator is an IDE like Visual Studio or Visual Studio Code. ```csharp -public sealed record TestMethodIdentifierProperty(string AssemblyFullName, string Namespace, string TypeName, string MethodName, string[] ParameterTypeFullNames, string ReturnTypeFullName) +public sealed record TestMethodIdentifierProperty( + string AssemblyFullName, + string Namespace, + string TypeName, + string MethodName, + string[] ParameterTypeFullNames, + string ReturnTypeFullName) ``` `TestMethodIdentifierProperty` is a unique identifier for a test method, adhering to the ECMA-335 standard. @@ -507,7 +573,8 @@ public sealed record TestMethodIdentifierProperty(string AssemblyFullName, strin > The data needed to create this property can be conveniently obtained using the .NET reflection feature, using types from the `System.Reflection` namespace. ```csharp -public sealed record TestMetadataProperty(string Key, string Value) +public sealed record TestMetadataProperty( + string Key, string Value) ``` `TestMetadataProperty` is utilized to convey the characteristics or *traits* of a `TestNode`. @@ -515,7 +582,8 @@ public sealed record TestMetadataProperty(string Key, string Value) ##### Discovery information ```csharp -public sealed record DiscoveredTestNodeStateProperty(string? Explanation = null) +public sealed record DiscoveredTestNodeStateProperty( + string? Explanation = null) { public static DiscoveredTestNodeStateProperty CachedInstance { get; } } @@ -528,7 +596,8 @@ This property is **required**. ##### Execution information ```csharp -public sealed record InProgressTestNodeStateProperty(string? Explanation = null) +public sealed record InProgressTestNodeStateProperty( + string? Explanation = null) { public static InProgressTestNodeStateProperty CachedInstance { get; } } @@ -555,7 +624,8 @@ The `TimingProperty` is utilized to relay timing details about the `TestNode` ex ***One and only one*** of the following properties is **required** per `TestNode` and communicates the result of the `TestNode` to the testing platform. ```csharp -public sealed record PassedTestNodeStateProperty(string? Explanation = null) +public sealed record PassedTestNodeStateProperty( + string? Explanation = null) { public static PassedTestNodeStateProperty CachedInstance { get; } } @@ -565,7 +635,8 @@ public sealed record PassedTestNodeStateProperty(string? Explanation = null) Take note of the handy cached value offered by the `CachedInstance` property. ```csharp -public sealed record SkippedTestNodeStateProperty(string? Explanation = null) +public sealed record SkippedTestNodeStateProperty( + string? Explanation = null) { public static SkippedTestNodeStateProperty CachedInstance { get; } } @@ -629,9 +700,7 @@ public sealed record CancelledTestNodeStateProperty As discussed in the [architecture](./unit-testing-platform-architecture.md) section, the initial step involves creating the `ITestApplicationBuilder` to register the testing framework and extensions with it. ```csharp -... -var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -... +var builder = await TestApplication.CreateBuilderAsync(args); ``` The `CreateBuilderAsync` method accepts an array of strings (`string[]`) named `args`. These arguments can be used to pass command-line options to all components of the testing platform (including built-in components, testing frameworks, and extensions), allowing for customization of their behavior. @@ -643,9 +712,8 @@ Arguments **must be prefixed** with a double dash `--`. For example, `--filter`. If a component such as a testing framework or an extension point wishes to offer custom command-line options, it can do so by implementing the `ICommandLineOptionsProvider` interface. This implementation can then be registered with the `ITestApplicationBuilder` via the registration factory of the `CommandLine` property, as shown: ```csharp -... -testApplicationBuilder.CommandLine.AddProvider(() => new CustomCommandLineOptions()); -... +builder.CommandLine.AddProvider( + static () => new CustomCommandLineOptions()); ``` In the example provided, `CustomCommandLineOptions` is an implementation of the `ICommandLineOptionsProvider` interface, This interface comprises the following members and data types: @@ -754,10 +822,12 @@ The `ITestSessionLifeTimeHandler` is an *in-process* extension that enables the To register a custom `ITestSessionLifeTimeHandler`, utilize the following API: ```csharp -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -... -testApplicationBuilder.TestHost.AddTestSessionLifetimeHandle(serviceProvider => new CustomTestSessionLifeTimeHandler()); -... +ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + +// ... + +builder.TestHost.AddTestSessionLifetimeHandle( + static serviceProvider => new CustomTestSessionLifeTimeHandler()); ``` The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. @@ -786,7 +856,7 @@ public interface ITestHostExtension : IExtension The `ITestSessionLifetimeHandler` is a type of `ITestHostExtension`, which serves as a base for all *test host* extensions. Like all other extension points, it also inherits from [IExtension](#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. -Let's describe the api: +Consider the following details for this API: `OnTestSessionStartingAsync`: This method is invoked prior to the commencement of the test session and receives the `SessionUid` object, which provides an opaque identifier for the current test session. @@ -803,11 +873,13 @@ The `ITestApplicationLifecycleCallbacks` is an *in-process* extension that enabl To register a custom `ITestApplicationLifecycleCallbacks`, utilize the following api: ```csharp -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -... -testApplicationBuilder.TestHost.AddTestApplicationLifecycleCallbacks(serviceProvider +ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + +// ... + +builder.TestHost.AddTestApplicationLifecycleCallbacks( + static serviceProvider => new CustomTestApplicationLifecycleCallbacks()); -... ``` The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. @@ -848,11 +920,12 @@ The `IDataConsumer` is an *in-process* extension capable of subscribing to and r To register a custom `IDataConsumer`, utilize the following api: ```csharp -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -... -testApplicationBuilder.TestHost.AddDataConsumer(serviceProvider - => new CustomDataConsumer()); -... +ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + +// ... + +builder.TestHost.AddDataConsumer( + static serviceProvider => new CustomDataConsumer()); ``` The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. @@ -926,7 +999,7 @@ internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer } ``` -Finally, the api takes a `CancellationToken` which the extension is expected to honor. +Finally, the API takes a `CancellationToken` which the extension is expected to honor. > [!IMPORTANT] > It's crucial to process the payload directly within the `ConsumeAsync` method. The [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) can manage both synchronous and asynchronous processing, coordinating the execution with the [testing framework](#test-framework-extension). Although the consumption process is entirely asynchronous and doesn't block the [IMessageBus.Push](./unit-testing-platform-architecture-services.md#the-imessagebus-service) at the time of writing, this is an implementation detail that may change in the future due to feature requirements. However, we aim to maintain this interface's simplicity and ensure that this method is always called once, eliminating the need for complex synchronization. Additionally, we automatically manage the scalability of the consumers. @@ -945,11 +1018,12 @@ The `ITestHostEnvironmentVariableProvider` is an *out-of-process* extension that To register a custom `ITestHostEnvironmentVariableProvider`, utilize the following API: ```csharp -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -... -testApplicationBuilder.TestHostControllers.AddEnvironmentVariableProvider(serviceProvider => - => new CustomEnvironmentVariableForTestHost()); -... +ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + +// ... + +builder.TestHostControllers.AddEnvironmentVariableProvider( + static serviceProvider => new CustomEnvironmentVariableForTestHost()); ``` The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. @@ -994,7 +1068,7 @@ public class EnvironmentVariable The `ITestHostEnvironmentVariableProvider` is a type of `ITestHostControllersExtension`, which serves as a base for all *test host controller* extensions. Like all other extension points, it also inherits from [IExtension](#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. -Let's describe the api: +Consider the details for this API: `UpdateAsync`: This update API provides an instance of the `IEnvironmentVariables` object, from which you can call the `SetVariable` or `RemoveVariable` methods. When using `SetVariable`, you must pass an object of type `EnvironmentVariable`, which requires the following specifications: @@ -1017,11 +1091,12 @@ The `ITestHostProcessLifetimeHandler` is an *out-of-process* extension that allo To register a custom `ITestHostProcessLifetimeHandler`, utilize the following API: ```csharp -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -... -testApplicationBuilder.TestHostControllers.AddProcessLifetimeHandler(serviceProvider => - new CustomMonitorTestHost()); -... +ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + +// ... + +builder.TestHostControllers.AddProcessLifetimeHandler( + static serviceProvider => new CustomMonitorTestHost()); ``` The factory utilizes the [IServiceProvider](./unit-testing-platform-architecture-services.md#the-imessagebus-service) to gain access to the suite of services offered by the testing platform. @@ -1049,7 +1124,7 @@ public interface ITestHostProcessInformation The `ITestHostProcessLifetimeHandler` is a type of `ITestHostControllersExtension`, which serves as a base for all *test host controller* extensions. Like all other extension points, it also inherits from [IExtension](#the-iextension-interface). Therefore, like any other extension, you can choose to enable or disable it using the `IExtension.IsEnabledAsync` API. -Let's describe the api: +Consider the following details for this API: `BeforeTestHostProcessStartAsync`: This method is invoked prior to the testing platform initiating the test hosts. @@ -1140,12 +1215,15 @@ internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ... Once you've created the `CompositeExtensionFactory` for your type, you can register it with both the `IDataConsumer` and `ITestSessionLifetimeHandler` APIs, which offer an overload for the `CompositeExtensionFactory`: ```csharp -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args); -... -CompositeExtensionFactory compositeExtensionFactory = new(serviceProvider => new CustomExtension()); -testApplicationBuilder.TestHost.AddTestSessionLifetimeHandle(compositeExtensionFactory); -testApplicationBuilder.TestHost.AddDataConsumer(compositeExtensionFactory); -... +ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + +// ... + +CompositeExtensionFactory factory = new(serviceProvider => new CustomExtension()); + +builder.TestHost.AddTestSessionLifetimeHandle(factory); + +builder.TestHost.AddDataConsumer(factory); ``` The factory constructor employs the [IServiceProvider](./unit-testing-platform-architecture-services.md) to access the services provided by the testing platform. From 1efb4d0b15eb7ee0204c48e5b841693c2a7cf20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Mon, 22 Jul 2024 16:38:27 +0200 Subject: [PATCH 10/19] Apply suggestions from code review Co-authored-by: David Pine --- docs/core/testing/unit-testing-platform-intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/testing/unit-testing-platform-intro.md b/docs/core/testing/unit-testing-platform-intro.md index aae8fa5473c5f..34a38c549584c 100644 --- a/docs/core/testing/unit-testing-platform-intro.md +++ b/docs/core/testing/unit-testing-platform-intro.md @@ -14,7 +14,7 @@ Microsoft.Testing.Platform is a lightweight and portable alternative to [VSTest] ## Microsoft.Testing.Platform pillars -This new testing platform is built on the .NET Developer Experience Testing team experience and aims at addressing the challenges encountered since the release of .NET Core in 2016. While there is a high level of compatibility between the .NET Framework and the .NET Core/.NET, some key features like the plugin system and the new possible form factors of .NET compilations have made it complex to evolve or fully support the new runtime feature with the current [VSTest platform](https://github.com/microsoft/vstest) architecture. +This new testing platform is built on the .NET Developer Experience Testing team's experience and aims to address the challenges encountered since the release of .NET Core in 2016. While there's a high level of compatibility between the .NET Framework and the .NET Core/.NET, some key features like the plugin-system and the new possible form factors of .NET compilations have made it complex to evolve or fully support the new runtime feature with the current [VSTest platform](https://github.com/microsoft/vstest) architecture. The main driving factors for the evolution of the new testing platform are detailed in the following: From eab3c253effa1a519b919f1c4fa51dbc2117f523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 14:18:07 +0200 Subject: [PATCH 11/19] Update docs/core/testing/unit-testing-platform-architecture-services.md Co-authored-by: David Pine --- .../core/testing/unit-testing-platform-architecture-services.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index db752b6b881c4..9dc6dd98843d5 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -8,7 +8,7 @@ ms.date: 07/11/2024 # Microsoft.Testing.Platform Services -The testing platform offers valuable services to both the testing framework and extension points. These services cater to common needs such as accessing the configuration, parsing and retrieving command-line arguments, obtaining the logging factory, and accessing the logging system, among others. `IServiceProvider` implements the [service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern) for the testing platform. +The testing platform offers valuable services to both the testing framework and extension points. These services cater to common needs such as accessing the configuration, parsing and retrieving command-line arguments, obtaining the logging factory, and accessing the logging system, among others. `IServiceProvider` implements the _service locator pattern_ for the testing platform. The `IServiceProvider` is derived directly from the base class library. From 4a0a08de668a49ef21f9a31c4fed3eb17c1b38fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 14:19:15 +0200 Subject: [PATCH 12/19] Update docs/core/testing/unit-testing-platform-architecture-services.md Co-authored-by: David Pine --- ...it-testing-platform-architecture-services.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index 9dc6dd98843d5..9216255d99713 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -345,9 +345,20 @@ public sealed class SystemConsoleColor : IColor Here's an example of how you might use the colored text with the *active* output device: ```csharp -IServiceProvider serviceProvider = ...get the service provider... -IOutputDevice outputDevice = serviceProvider.GetOutputDevice(); -await outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData($"TestingFramework version '{Version}' running tests with parallelism of {_dopValue}") { ForegroundColor = new SystemConsoleColor() { ConsoleColor = ConsoleColor.Green } }); +IServiceProvider provider = null; // Get the service provider... + +IOutputDevice outputDevice = provider.GetOutputDevice(); + +await outputDevice.DisplayAsync( + this, + new FormattedTextOutputDeviceData( + $"TestingFramework version '{Version}' running tests with parallelism of {_dopValue}") + { + ForegroundColor = new SystemConsoleColor + { + ConsoleColor = ConsoleColor.Green + } + }); ``` Beyond the standard use of colored text, the main advantage of `IOutputDevice` and `IOutputDeviceData` is that the *output device* is entirely independent and unknown to the user. This allows for the development of complex user interfaces. For example, it's entirely feasible to implement a *real-time* web application that displays the progress of tests. From 69a84f9d5c0c10544e4bed5873345f3d3cb80e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 14:21:27 +0200 Subject: [PATCH 13/19] Update docs/core/testing/unit-testing-platform-architecture-extensions.md Co-authored-by: David Pine --- .../testing/unit-testing-platform-architecture-extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index bf439c254aeff..1ad07211952e3 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -90,7 +90,7 @@ ITestApplicationBuilder RegisterTestFramework( The `RegisterTestFramework` API expects two factories: -1. `Func`: This is a lambda function that accepts an object implementing the [`IServiceProvider`](./unit-testing-platform-architecture-services.md) interface and returns an object implementing the [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) interface. The [`IServiceProvider`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) provides access to platform services such as configurations, loggers, command line arguments, etc. +1. `Func`: This is a delegate that accepts an object implementing the [`IServiceProvider`](./unit-testing-platform-architecture-services.md) interface and returns an object implementing the [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) interface. The [`IServiceProvider`](./unit-testing-platform-architecture-services.md#the-imessagebus-service) provides access to platform services such as configurations, loggers, and command line arguments. The [`ITestFrameworkCapabilities`](./unit-testing-platform-architecture-capabilities.md) interface is used to announce the capabilities supported by the testing framework to the platform and extensions. It allows the platform and extensions to interact correctly by implementing and supporting specific behaviors. For a better understanding of the [concept of capabilities](./unit-testing-platform-architecture-capabilities.md), refer to the respective section. From 4755c0565a8fe5edbbc74807bfbdf31c67fd8a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 14:23:48 +0200 Subject: [PATCH 14/19] Apply suggestions from code review Co-authored-by: David Pine --- ...-testing-platform-architecture-services.md | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index 9216255d99713..a5cd3960a50ad 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -40,7 +40,7 @@ public static class ServiceProviderExtensions Most of the registration factories exposed by extension points, which can be registered using the `ITestApplicationBuilder` during the setup of the testing application, provide access to the `IServiceProvider`. -For example, we encountered it earlier when discussing [registering the testing framework](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework). +For more information, see [registering the testing framework](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework). ```csharp ITestApplicationBuilder RegisterTestFramework( @@ -48,7 +48,7 @@ ITestApplicationBuilder RegisterTestFramework( Func adapterFactory); ``` -As observed above, both the `capabilitiesFactory` and the `adapterFactory` supply the `IServiceProvider` as a parameter. +In the preceding code, both the `capabilitiesFactory` and the `adapterFactory` supply the `IServiceProvider` as a parameter. ## The `IConfiguration` service @@ -83,11 +83,13 @@ The JSON file follows a hierarchical structure. To access child properties, you The code snippet would look something like this: ```csharp -IServiceProvider serviceProvider = ...get the service provider... +IServiceProvider serviceProvider = null; // Get the service provider... + IConfiguration configuration = serviceProvider.GetConfiguration(); -if (configuration["CustomTestingFramework:DisableParallelism"] == bool.TrueString) + +if (bool.TryParse(configuration["CustomTestingFramework:DisableParallelism"], out var value) && value is true) { - ... + // ... } ``` @@ -107,8 +109,10 @@ In the case of an array, such as: The syntax to access to the fist element ("ThreadPool") is: ```csharp -IServiceProvider serviceProvider = ...get the service provider... +IServiceProvider serviceProvider = null; // Get the service provider... + IConfiguration configuration = serviceProvider.GetConfiguration(); + var fistElement = configuration["CustomTestingFramework:Engine:0"]; ``` @@ -128,9 +132,11 @@ setx CustomTestingFramework__DisableParallelism=True You can choose not to use the environment variable configuration source when creating the `ITestApplicationBuilder`: ```csharp -var testApplicationOptions = new TestApplicationOptions(); -testApplicationOptions.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource = false; -ITestApplicationBuilder testApplicationBuilder = await TestApplication.CreateBuilderAsync(args, testApplicationOptions); +var options = new TestApplicationOptions(); + +options.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource = false; + +ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args, options); ``` ## The `ICommandLineOptions` service @@ -141,7 +147,9 @@ The `ICommandLineOptions` service is utilized to fetch details regarding the com public interface ICommandLineOptions { bool IsOptionSet(string optionName); - bool TryGetOptionArgumentList(string optionName, out string[]? arguments); + + bool TryGetOptionArgumentList( + string optionName, out string[]? arguments); } ``` @@ -233,15 +241,21 @@ Here's an example of how you might use the logging API: ```csharp ... -IServiceProvider serviceProvider = ...get the service provider... -ILoggerFactory loggerFactory = serviceProvider.GetLoggerFactory(); -ILogger logger = loggerFactory.CreateLogger(); -... -if (_logger.IsEnabled(LogLevel.Information)) +IServiceProvider provider = null; // Get the service provider... + +ILoggerFactory factory = provider.GetLoggerFactory(); + +ILogger logger = factory.CreateLogger(); + +// ... + +if (logger.IsEnabled(LogLevel.Information)) { - await _logger.LogInformationAsync($"Executing request of type '{context.Request}'"); + await logger.LogInformationAsync( + $"Executing request of type '{context.Request}'"); } -... + +// ... ``` Keep in mind that to prevent unnecessary allocation, you should check if the level is *enabled* using the `ILogger.IsEnabled(LogLevel)` API. @@ -250,7 +264,7 @@ Keep in mind that to prevent unnecessary allocation, you should check if the lev The message bus service is the central mechanism that facilitates information exchange between the test framework and its extensions. -The message bus of the testing platform employs the publish-subscribe pattern, as described here: . +The message bus of the testing platform employs the _publish-subscribe pattern_. The overarching structure of the shared bus is as follows: @@ -258,7 +272,7 @@ The overarching structure of the shared bus is as follows: As illustrated in the diagram, which includes an extensions and a test framework, there are two potential actions: pushing information to the bus or consuming information from the bus. -The `IMessageBus` satisfied the *pushing action* to the bus and the api is: +The `IMessageBus` satisfied the *pushing action* to the bus and the API is: ```csharp public interface IMessageBus @@ -278,11 +292,12 @@ public interface IData } ``` -Let's discuss the parameters: +Consider the following details about the parameters: * `IDataProducer`: The `IDataProducer` communicates to the message bus the `Type` of information it can supply and establishes ownership through inheritance from the base interface [IExtension](./unit-testing-platform-architecture-extensions.md#the-iextension-interface). This implies that you can't indiscriminately push data to the message bus; you must declare the data type produced in advance. If you push unexpected data, an exception will be triggered. * `IData`: This interface serves as a placeholder where you only need to provide descriptive details such as the name and a description. The interface doesn't reveal much about the data's nature, which is intentional. It implies that the test framework and extensions can push any type of data to the bus, and this data can be consumed by any registered extension or the test framework itself. + This approach facilitates the evolution of the information exchange process, preventing breaking changes when an extension is unfamiliar with new data. **It allows different versions of extensions and the test framework to operate in harmony, based on their mutual understanding**. The opposite end of the bus is what we refer to as a [consumer](./unit-testing-platform-architecture-extensions.md#the-idataconsumer-extensions), which is subscribed to a specific type of data and can thus consume it. @@ -300,6 +315,7 @@ The most traditional example of an *output device* is the console output. > While the testing platform is engineered to support custom *output devices*, currently, this extension point is not available. To transmit data to the *output device*, you must obtain the `IOutputDevice` from the [`IServiceProvider`](#microsofttestingplatform-services). + The API consists of: ```csharp @@ -318,6 +334,7 @@ public interface IOutputDeviceData ``` The `IOutputDeviceDataProducer` extends the [`IExtension`](./unit-testing-platform-architecture-extensions.md#the-iextension-interface) and provides information about the sender to the *output device*. + The `IOutputDeviceData` serves as a placeholder interface. The concept behind `IOutputDevice` is to accommodate more intricate information than just colored text. For instance, it could be a complex object that can be graphically represented. The testing platform, by default, offers a traditional colored text model for the `IOutputDeviceData` object: From 2a0340d88530b8b9dcad3f46014777dfdc903aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 14:17:49 +0200 Subject: [PATCH 15/19] Do not use "we" --- ...it-testing-platform-architecture-extensions.md | 15 +++++++-------- ...unit-testing-platform-architecture-services.md | 6 ++---- .../testing/unit-testing-platform-architecture.md | 7 ++++--- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index 1ad07211952e3..7d19dfb17a5c2 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -216,7 +216,7 @@ public sealed class ExecuteRequestContext } ``` -`IRequest`: This is the base interface for any type of request. We should think about the test framework as an **in-process stateful server** where the lifecycle is: +`IRequest`: This is the base interface for any type of request. You should think about the test framework as an **in-process stateful server** where the lifecycle is: ```mermaid sequenceDiagram @@ -456,7 +456,7 @@ public interface IProperty } ``` -* `TestNodeUpdateMessage`: The `TestNodeUpdateMessage` consists of two properties: a `TestNode`, which we will discuss in this section, and a `ParentTestNodeUid`. The `ParentTestNodeUid` indicates that a test may have a parent test, introducing the concept of a **test tree** where `TestNode`s can be arranged in relation to each other. This structure allows for future enhancements and features based on the *tree* relationship between the nodes. If your test framework doesn't require a test tree structure, you can opt not to use it and simply set it to null, resulting in a straightforward flat list of `TestNode`s. +* `TestNodeUpdateMessage`: The `TestNodeUpdateMessage` consists of two properties: a `TestNode` and a `ParentTestNodeUid`. The `ParentTestNodeUid` indicates that a test may have a parent test, introducing the concept of a **test tree** where `TestNode`s can be arranged in relation to each other. This structure allows for future enhancements and features based on the *tree* relationship between the nodes. If your test framework doesn't require a test tree structure, you can opt not to use it and simply set it to null, resulting in a straightforward flat list of `TestNode`s. * `TestNode`: The `TestNode` is composed of three properties, one of which is the `Uid` of type `TestNodeUid`. This `Uid` serves as the **UNIQUE STABLE ID** for the node. The term **UNIQUE STABLE ID** implies that the same `TestNode` should maintain an **IDENTICAL** `Uid` across different runs and operating systems. The `TestNodeUid` is an **arbitrary opaque string** that the testing platform accepts as is. @@ -516,7 +516,7 @@ In this segment, we'll elucidate the various well-known `IProperty` options and If you're looking for a comprehensive list of well-known properties, you can find it [here](https://github.com/microsoft/testfx/blob/main/src/Platform/Microsoft.Testing.Platform/Messages/TestNodeProperties.cs). If you notice that a property description is missing, please don't hesitate to file an issue. -We can divide the properties in: +These properties can be divided in the following categories: 1. [*Generic information*](#generic-information): Properties that can be included in any kind of request. 1. [*Discovery information*](#discovery-information): Properties that are supplied during a `DiscoverTestExecutionRequest` discovery request. @@ -778,7 +778,8 @@ For examples, refer to the [System.CommandLine arity table](https://learn.micros `ICommandLineOptionsProvider.ValidateOptionArgumentsAsync`: This method is employed to *validate* the argument provided by the user. -For instance, if we have a parameter named `--dop` that represents the degree of parallelism for our custom testing framework, a user might input `--dop 0`. In this scenario, the value 0 would be invalid because we anticipate a degree of parallelism of 1 or more. By using `ValidateOptionArgumentsAsync`, we can perform upfront validation and return an error message if necessary. +For instance, if you have a parameter named `--dop` that represents the degree of parallelism for our custom testing framework, a user might input `--dop 0`. In this scenario, the value `0` would be invalid because it is expected to have a degree of parallelism of `1` or more. By using `ValidateOptionArgumentsAsync`, you can perform upfront validation and return an error message if necessary. + A possible implementation for the sample above could be: ```csharp @@ -1002,7 +1003,7 @@ internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer Finally, the API takes a `CancellationToken` which the extension is expected to honor. > [!IMPORTANT] -> It's crucial to process the payload directly within the `ConsumeAsync` method. The [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) can manage both synchronous and asynchronous processing, coordinating the execution with the [testing framework](#test-framework-extension). Although the consumption process is entirely asynchronous and doesn't block the [IMessageBus.Push](./unit-testing-platform-architecture-services.md#the-imessagebus-service) at the time of writing, this is an implementation detail that may change in the future due to feature requirements. However, we aim to maintain this interface's simplicity and ensure that this method is always called once, eliminating the need for complex synchronization. Additionally, we automatically manage the scalability of the consumers. +> It's crucial to process the payload directly within the `ConsumeAsync` method. The [IMessageBus](./unit-testing-platform-architecture-services.md#the-imessagebus-service) can manage both synchronous and asynchronous processing, coordinating the execution with the [testing framework](#test-framework-extension). Although the consumption process is entirely asynchronous and doesn't block the [IMessageBus.Push](./unit-testing-platform-architecture-services.md#the-imessagebus-service) at the time of writing, this is an implementation detail that may change in the future due to future requirements. However, the platform ensures that this method is always called once, eliminating the need for complex synchronization, as well as managing the scalability of the consumers. @@ -1142,9 +1143,7 @@ The `ITestHostProcessInformation` interface provides the following details: ## Extensions execution order -The testing platform consists of a [testing framework](#test-framework-extension) and any number of extensions that can operate [*in-process*](#microsofttestingplatform-extensibility) or [*out-of-process*](#microsofttestingplatform-extensibility). This document outlines the **sequence of calls** to all potential extensibility points to provide clarity on when a feature is anticipated to be invoked. - -While a *sequence* could be used to depict this, we opt for a straightforward order of invocation calls, which allows for a more comprehensive commentary on the workflow. +The testing platform consists of a [testing framework](#test-framework-extension) and any number of extensions that can operate [*in-process*](#microsofttestingplatform-extensibility) or [*out-of-process*](#microsofttestingplatform-extensibility). This document outlines the **sequence of calls** to all potential extensibility points to provide clarity on when a feature is anticipated to be invoked: 1. [ITestHostEnvironmentVariableProvider.UpdateAsync](#the-itesthostenvironmentvariableprovider-extensions) : Out-of-process 1. [ITestHostEnvironmentVariableProvider.ValidateTestHostEnvironmentVariablesAsync](#the-itesthostenvironmentvariableprovider-extensions) : Out-of-process diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index a5cd3960a50ad..fd416ffccda9d 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -38,9 +38,7 @@ public static class ServiceProviderExtensions } ``` -Most of the registration factories exposed by extension points, which can be registered using the `ITestApplicationBuilder` during the setup of the testing application, provide access to the `IServiceProvider`. - -For more information, see [registering the testing framework](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework). +Most of the registration factories exposed by extension points provide access to the `IServiceProvider`: For example, when [registering the testing framework](./unit-testing-platform-architecture-extensions.md#registering-a-testing-framework), the `IServiceProvider` is passed as a parameter to the factory method. ```csharp ITestApplicationBuilder RegisterTestFramework( @@ -300,7 +298,7 @@ Consider the following details about the parameters: This approach facilitates the evolution of the information exchange process, preventing breaking changes when an extension is unfamiliar with new data. **It allows different versions of extensions and the test framework to operate in harmony, based on their mutual understanding**. -The opposite end of the bus is what we refer to as a [consumer](./unit-testing-platform-architecture-extensions.md#the-idataconsumer-extensions), which is subscribed to a specific type of data and can thus consume it. +The opposite end of the bus is referred to as a [consumer](./unit-testing-platform-architecture-extensions.md#the-idataconsumer-extensions), which is subscribed to a specific type of data and can thus consume it. > [!IMPORTANT] > Always use *await* the call to `PublishAsync`. If you don't, the `IData` might not be processed correctly by the testing platform and extensions, which could lead to subtle bugs. It's only after you've returned from the *await* that you can be assured that the `IData` has been queued for processing on the message bus. Regardless of the extension point you're working on, ensure that you've awaited all `PublishAsync` calls before exiting the extension. For example, if you're implementing the [`testing framework`](./unit-testing-platform-architecture-extensions.md#creating-a-testing-framework), you should not call `Complete` on the [requests](./unit-testing-platform-architecture-extensions.md#handling-requests) until you've awaited all `PublishAsync` calls for that specific request. diff --git a/docs/core/testing/unit-testing-platform-architecture.md b/docs/core/testing/unit-testing-platform-architecture.md index 3c625d7bf61f2..09d3d2ac8aecd 100644 --- a/docs/core/testing/unit-testing-platform-architecture.md +++ b/docs/core/testing/unit-testing-platform-architecture.md @@ -41,7 +41,7 @@ By following this structured approach, you will gain a comprehensive understandi ## Step 1: Register and Run a simple test application -To introduce the architecture of the testing platform, we will use the classic console application (for Windows) as the host. The samples in this document are written in C#, but you can use the testing platform with any language that supports the .NET Ecma specification, and run on any OS supported by .NET. To use the platform, simply reference the `Microsoft.Testing.Platform.dll` assembly, which can be consumed through the official NuGet package available at . +To introduce the architecture of the testing platform, this document will use the classic console application (for Windows) as the host. The samples in this document are written in C#, but you can use the testing platform with any language that supports the .NET Ecma specification, and run on any OS supported by .NET. To use the platform, simply reference the `Microsoft.Testing.Platform.dll` assembly, which can be consumed through the official NuGet package available at . In a console project `Contoso.UnitTests.exe` the following `Main` method defines the entry point: @@ -60,7 +60,8 @@ public static async Task Main(string[] args) This code includes everything needed to execute a test session, except for registering a test framework such as MSTest through `RegisterTestFramework`. This code is shown and explained in later sections. -Please also note that in a typical setup this code is automatically generated through MSBuild, and is not visible in your project. By typical setup we mean for example generating new project from .NET 9 `mstest` template via `dotnet new mstest`. +> [!NOTE] +> The default behavior is to have the entry point automatically generated by the testing platform MSBuild task. This is done by adding the `Microsoft.Testing.Platform.MSBuild` package to your project. When `Contoso.UnitTests.exe` application is started a standard Windows process is created, and the testing platform interacts with the registered testing framework to execute the testing session. @@ -123,7 +124,7 @@ public static class TestApplicationBuilderExtensions The parameters for the api `AddEnvironmentVariableProvider` will be explained in later sections. -When we run `Contoso.UnitTests.exe` this time, the testing platform detects that a `TestHostController` extension is registered. As a result, it starts another instance of the `Contoso.UnitTests.exe` process as a child process. This is done to properly set the environment variables as required by the extension registered with the `AddEnvironmentVariableProvider` API. +When running `Contoso.UnitTests.exe` this time, the testing platform detects that a `TestHostController` extension is registered. As a result, it starts another instance of the `Contoso.UnitTests.exe` process as a child process. This is done to properly set the environment variables as required by the extension registered with the `AddEnvironmentVariableProvider` API. The process layout looks like this: From db87575057483dedc65b957e6d911225926cbdac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 14:24:57 +0200 Subject: [PATCH 16/19] Refactoring --- .../testing/unit-testing-platform-architecture-extensions.md | 2 ++ .../testing/unit-testing-platform-architecture-services.md | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index 7d19dfb17a5c2..0bc0283e97f39 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -76,6 +76,8 @@ public interface IExtension ## Test Framework extension +The test framework is the primary extension that provides the testing platform with the ability to discover and execute tests. The test framework is responsible for communicating the results of the tests back to the testing platform. The test framework is the only mandatory extension required to execute a testing session. + ### Register a testing framework This section explains how to register the test framework with the testing platform. You register only one testing framework per test application builder using the `TestApplication.RegisterTestFramework` API as shown in [the testing platform architecture](./unit-testing-platform-architecture.md) documentation. diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index fd416ffccda9d..e597c9f6669bf 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -366,8 +366,7 @@ IOutputDevice outputDevice = provider.GetOutputDevice(); await outputDevice.DisplayAsync( this, - new FormattedTextOutputDeviceData( - $"TestingFramework version '{Version}' running tests with parallelism of {_dopValue}") + new FormattedTextOutputDeviceData($"TestingFramework version '{Version}' running tests with parallelism of {_dopValue}") { ForegroundColor = new SystemConsoleColor { From 63609b1ea10d675afbd819063008464ea326a465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 14:41:48 +0200 Subject: [PATCH 17/19] Mermaid links + pngs --- ...ndlineoptionsprovider-sequence-diagram.png | Bin 0 -> 39214 bytes .../platform-testhostcontroller-testhost.png | Bin 0 -> 30823 bytes docs/core/testing/media/platorm-testhost.png | Bin 0 -> 21110 bytes .../media/test-framework-sequence-diagram.png | Bin 0 -> 73330 bytes ...esting-platform-architecture-extensions.md | 33 +++--------------- .../unit-testing-platform-architecture.md | 12 +++---- 6 files changed, 8 insertions(+), 37 deletions(-) create mode 100644 docs/core/testing/media/icommandlineoptionsprovider-sequence-diagram.png create mode 100644 docs/core/testing/media/platform-testhostcontroller-testhost.png create mode 100644 docs/core/testing/media/platorm-testhost.png create mode 100644 docs/core/testing/media/test-framework-sequence-diagram.png diff --git a/docs/core/testing/media/icommandlineoptionsprovider-sequence-diagram.png b/docs/core/testing/media/icommandlineoptionsprovider-sequence-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..6f69457b7aff68d8785693e31de215a94f9e02fa GIT binary patch literal 39214 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 literal 0 HcmV?d00001 diff --git a/docs/core/testing/media/platform-testhostcontroller-testhost.png b/docs/core/testing/media/platform-testhostcontroller-testhost.png new file mode 100644 index 0000000000000000000000000000000000000000..9a5f2302d91f5efb84aef4582c85eb2fb90cd1e8 GIT binary patch literal 30823 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ℜ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: adapterFactory() from 'RegisterTestFramework' - ITestFramework-->>Testing platform: - Testing platform->>ITestFramework: CreateTestSessionAsync(CreateTestSessionContext) - ITestFramework-->>Testing platform: CreateTestSessionResult - Testing platform->>ITestFramework: ExecuteRequestAsync(ExecuteRequestContext_1) - Testing platform->>ITestFramework: ExecuteRequestAsync(ExecuteRequestContext_2) - ITestFramework->>IMessageBus: PublishAsync() for ExecuteRequestContext_1 - Testing platform->>ITestFramework: ExecuteRequestAsync(ExecuteRequestContext_3) - ITestFramework->>IMessageBus: PublishAsync() for ExecuteRequestContext_3 - ITestFramework->>IMessageBus: PublishAsync() for ExecuteRequestContext_2 - ITestFramework->>IMessageBus: PublishAsync() for ExecuteRequestContext_2 - ITestFramework->>IMessageBus: PublishAsync() for ... - ITestFramework->>ExecuteRequestContext_1: Complete() - ITestFramework->>ExecuteRequestContext_3: Complete() - ITestFramework->>ExecuteRequestContext_2: Complete() - Testing platform->>ITestFramework: CloseTestSessionAsync(CloseTestSessionContext) - ITestFramework-->>Testing platform: CloseTestSessionResult -``` + +:::image type="content" source="./media/test-framework-sequence-diagram.png" lightbox="./media/test-framework-sequence-diagram.png" alt-text="A sequence diagram representing the lifecycle of the test framework."::: The preceding diagram illustrates that the testing platform issues three requests after creating the test framework instance. The test framework processes these requests and utilizes the `IMessageBus` service, which is included in the request itself, to deliver the result for each specific request. Once a particular request has been handled, the test framework invokes the `Complete()` method on it, indicating to the testing platform that the request has been fulfilled. The testing platform monitors all dispatched requests. Once all requests have been fulfilled, it invokes `CloseTestSessionAsync` and disposes of the instance (if `IDisposable/IAsyncDisposable` is implemented). @@ -748,15 +730,8 @@ As observed, the `ICommandLineOptionsProvider` extends the [`IExtension`](#the-i The order of execution of the `ICommandLineOptionsProvider` is: -```mermaid -sequenceDiagram - Testing platform->>ICommandLineOptionsProvider: GetCommandLineOptions() - ICommandLineOptionsProvider-->>Testing platform: returns the list of `CommandLineOption` - Testing platform->>ICommandLineOptionsProvider: ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) validate every option argument - ICommandLineOptionsProvider-->>Testing platform: returns a `ValidationResult` indicating success or failure. - Testing platform->>ICommandLineOptionsProvider: ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) Ensure the consistency of all arguments in unison. - ICommandLineOptionsProvider-->>Testing platform: returns a `ValidationResult` indicating success or failure. -``` + +:::image type="content" source="./media/icommandlineoptionsprovider-sequence-diagram.png" lightbox="./media/icommandlineoptionsprovider-sequence-diagram.png" alt-text="A diagram representing the order of execution of the 'ICommandLineOptionsProvider' interface."::: Let's examine the apis and their mean: diff --git a/docs/core/testing/unit-testing-platform-architecture.md b/docs/core/testing/unit-testing-platform-architecture.md index 09d3d2ac8aecd..137bcc844b93d 100644 --- a/docs/core/testing/unit-testing-platform-architecture.md +++ b/docs/core/testing/unit-testing-platform-architecture.md @@ -67,10 +67,8 @@ When `Contoso.UnitTests.exe` application is started a standard Windows process i A single process is created to carry out this work: -```mermaid -graph TD; - TestHost:'Contoso.UnitTests.exe'; -``` + +:::image type="content" source="./media/platform-testhost.png" lightbox="./media/platform-testhost.png" alt-text="A diagram representing the test host process."::: The testing platform includes a built-in display device that writes the testing session information in the terminal, similar to: @@ -128,10 +126,8 @@ When running `Contoso.UnitTests.exe` this time, the testing platform detects tha The process layout looks like this: -```mermaid -graph TD; - TestHostController:'Contoso.UnitTests.exe'-->TestHost:'Contoso.UnitTests.exe; -``` + +:::image type="content" source="./media/platform-testhostcontroller-testhost.png" lightbox="./media/platform-testhostcontroller-testhost.png" alt-text="A diagram representing the process layout of the test host and test host controller."::: > [!NOTE] > The provided example assumes a console application layout, which handles the start process correctly and propagates all command line arguments to the child process. From ab8c42c660531ff9e386b5e12b6e6985b11dc0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 15:05:40 +0200 Subject: [PATCH 18/19] Refactor --- ...ting-platform-architecture-capabilities.md | 4 +- ...esting-platform-architecture-extensions.md | 265 +++++++++++++----- ...-testing-platform-architecture-services.md | 89 ++++-- 3 files changed, 262 insertions(+), 96 deletions(-) diff --git a/docs/core/testing/unit-testing-platform-architecture-capabilities.md b/docs/core/testing/unit-testing-platform-architecture-capabilities.md index e519510fcdb3c..5fdc8e51a14ac 100644 --- a/docs/core/testing/unit-testing-platform-architecture-capabilities.md +++ b/docs/core/testing/unit-testing-platform-architecture-capabilities.md @@ -77,10 +77,10 @@ The hypothetical code fragment inside the extension could be something like: ```csharp IServiceProvider provider = null; // TODO: Get IServiceProvider. -ITestFrameworkCapabilities capabilities = serviceProvider.GetRequiredService(); +var capabilities = serviceProvider.GetRequiredService(); // Utilize the `GetCapability` API to search for the specific capability to query. -IDisableParallelismCapability? capability = capabilities.GetCapability(); +var capability = capabilities.GetCapability(); if (capability is null) { // Capability not supported... diff --git a/docs/core/testing/unit-testing-platform-architecture-extensions.md b/docs/core/testing/unit-testing-platform-architecture-extensions.md index d1fb6c0e4ad64..1a2c518a30fbc 100644 --- a/docs/core/testing/unit-testing-platform-architecture-extensions.md +++ b/docs/core/testing/unit-testing-platform-architecture-extensions.md @@ -31,7 +31,7 @@ Due to these reasons, the extension points are categorized into two types: ```csharp // ... - ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + var builder = await TestApplication.CreateBuilderAsync(args); builder.TestHost.AddXXX(...); // ... ``` @@ -41,7 +41,7 @@ Due to these reasons, the extension points are categorized into two types: You can register *out-of-process extensions* via the `ITestApplicationBuilder.TestHostControllers`. ```csharp - ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); + var builder = await TestApplication.CreateBuilderAsync(args); builder.TestHostControllers.AddXXX(...); ``` @@ -517,20 +517,25 @@ Optional properties, on the other hand, enhance the testing experience by provid ```csharp public record KeyValuePairStringProperty( - string Key, string Value) : IProperty; + string Key, + string Value) + : IProperty; ``` The `KeyValuePairStringProperty` stands for a general key/value pair data. ```csharp public record struct LinePosition( - int Line, int Column); + int Line, + int Column); public record struct LinePositionSpan( - LinePosition Start, LinePosition End); + LinePosition Start, + LinePosition End); public abstract record FileLocationProperty( - string FilePath, LinePositionSpan LineSpan) + string FilePath, + LinePositionSpan LineSpan) : IProperty; public sealed record TestFileLocationProperty( @@ -558,7 +563,8 @@ public sealed record TestMethodIdentifierProperty( ```csharp public sealed record TestMetadataProperty( - string Key, string Value) + string Key, + string Value) ``` `TestMetadataProperty` is utilized to convey the characteristics or *traits* of a `TestNode`. @@ -591,14 +597,33 @@ The `InProgressTestNodeStateProperty` informs the testing platform that the `Tes Take note of the handy cached value offered by the `CachedInstance` property. ```csharp -public readonly record struct TimingInfo(DateTimeOffset StartTime, DateTimeOffset EndTime, TimeSpan Duration); -public sealed record StepTimingInfo(string Id, string Description, TimingInfo Timing); +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) - public TimingProperty(TimingInfo globalTiming, StepTimingInfo[] stepTimings) + : this(globalTiming, []) + { + } + + public TimingProperty( + TimingInfo globalTiming, + StepTimingInfo[] stepTimings) + { + GlobalTiming = globalTiming; + StepTimings = stepTimings; + } + public TimingInfo GlobalTiming { get; } + public StepTimingInfo[] StepTimings { get; } } ``` @@ -609,9 +634,11 @@ The `TimingProperty` is utilized to relay timing details about the `TestNode` ex ```csharp public sealed record PassedTestNodeStateProperty( - string? Explanation = null) + string? Explanation = null) + : TestNodeStateProperty(Explanation) { - public static PassedTestNodeStateProperty CachedInstance { get; } + public static PassedTestNodeStateProperty CachedInstance + { get; } = new PassedTestNodeStateProperty(); } ``` @@ -620,9 +647,11 @@ Take note of the handy cached value offered by the `CachedInstance` property. ```csharp public sealed record SkippedTestNodeStateProperty( - string? Explanation = null) + string? Explanation = null) + : TestNodeStateProperty(Explanation) { - public static SkippedTestNodeStateProperty CachedInstance { get; } + public static SkippedTestNodeStateProperty CachedInstance + { get; } = new SkippedTestNodeStateProperty(); } ``` @@ -630,10 +659,26 @@ public sealed record SkippedTestNodeStateProperty( Take note of the handy cached value offered by the `CachedInstance` property. ```csharp -public sealed record FailedTestNodeStateProperty +public sealed record FailedTestNodeStateProperty : TestNodeStateProperty { + public FailedTestNodeStateProperty() + : base(default(string)) + { + } + public FailedTestNodeStateProperty(string explanation) - public FailedTestNodeStateProperty(Exception exception, string? explanation = null) + : base(explanation) + { + } + + public FailedTestNodeStateProperty( + Exception exception, + string? explanation = null) + : base(explanation ?? exception.Message) + { + Exception = exception; + } + public Exception? Exception { get; } } ``` @@ -641,10 +686,26 @@ public sealed record FailedTestNodeStateProperty `FailedTestNodeStateProperty` informs the testing platform that this `TestNode` is failed after an assertion. ```csharp -public sealed record ErrorTestNodeStateProperty +public sealed record ErrorTestNodeStateProperty : TestNodeStateProperty { + public ErrorTestNodeStateProperty() + : base(default(string)) + { + } + public ErrorTestNodeStateProperty(string explanation) - public ErrorTestNodeStateProperty(Exception exception, string? explanation = null) + : base(explanation) + { + } + + public ErrorTestNodeStateProperty( + Exception exception, + string? explanation = null) + : base(explanation ?? exception.Message) + { + Exception = exception; + } + public Exception? Exception { get; } } ``` @@ -652,11 +713,28 @@ public sealed record ErrorTestNodeStateProperty `ErrorTestNodeStateProperty` informs the testing platform that this `TestNode` has failed. This type of failure is different from the `FailedTestNodeStateProperty`, which is used for assertion failures. For example, you can report issues like test initialization errors with `ErrorTestNodeStateProperty`. ```csharp -public sealed record TimeoutTestNodeStateProperty +public sealed record TimeoutTestNodeStateProperty : TestNodeStateProperty { + public TimeoutTestNodeStateProperty() + : base(default(string)) + { + } + public TimeoutTestNodeStateProperty(string explanation) - public TimeoutTestNodeStateProperty(Exception exception, string? explanation = null) + : base(explanation) + { + } + + public TimeoutTestNodeStateProperty( + Exception exception, + string? explanation = null) + : base(explanation ?? exception.Message) + { + Exception = exception; + } + public Exception? Exception { get; } + public TimeSpan? Timeout { get; init; } } ``` @@ -664,10 +742,26 @@ public sealed record TimeoutTestNodeStateProperty `TimeoutTestNodeStateProperty` informs the testing platform that this `TestNode` is failed for a timeout reason. You can report the timeout using the `Timeout` property. ```csharp -public sealed record CancelledTestNodeStateProperty +public sealed record CancelledTestNodeStateProperty : TestNodeStateProperty { + public CancelledTestNodeStateProperty() + : base(default(string)) + { + } + public CancelledTestNodeStateProperty(string explanation) - public CancelledTestNodeStateProperty(Exception exception, string? explanation = null) + : base(explanation) + { + } + + public CancelledTestNodeStateProperty( + Exception exception, + string? explanation = null) + : base(explanation ?? exception.Message) + { + Exception = exception; + } + public Exception? Exception { get; } } ``` @@ -676,6 +770,8 @@ public sealed record CancelledTestNodeStateProperty ## Other extensibility points +The testing platform provides additional extensibility points that allow you to customize the behavior of the platform and the test framework. These extensibility points are optional and can be used to enhance the testing experience. + ### The `ICommandLineOptionsProvider` extensions > [!NOTE] @@ -706,23 +802,32 @@ In the example provided, `CustomCommandLineOptions` is an implementation of the public interface ICommandLineOptionsProvider : IExtension { IReadOnlyCollection GetCommandLineOptions(); - Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments); - Task ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions); + + Task ValidateOptionArgumentsAsync( + CommandLineOption commandOption, + string[] arguments); + + Task ValidateCommandLineOptionsAsync( + ICommandLineOptions commandLineOptions); } public sealed class CommandLineOption { - public CommandLineOption(string name, string description, ArgumentArity arity, bool isHidden) 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); + + bool TryGetOptionArgumentList( + string optionName, + out string[]? arguments); } ``` @@ -760,18 +865,20 @@ For instance, if you have a parameter named `--dop` that represents the degree o A possible implementation for the sample above could be: ```csharp - public Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) +public Task ValidateOptionArgumentsAsync( + CommandLineOption commandOption, + string[] arguments) +{ + if (commandOption.Name == "dop") { - if (commandOption.Name == "dop") + if (!int.TryParse(arguments[0], out int dopValue) || dopValue <= 0) { - if (!int.TryParse(arguments[0], out int dopValue) || dopValue <= 0) - { - return ValidationResult.InvalidTask("--dop must be a positive integer"); - } + return ValidationResult.InvalidTask("--dop must be a positive integer"); } - - return ValidationResult.ValidTask; } + + return ValidationResult.ValidTask; +} ``` `ICommandLineOptionsProvider.ValidateCommandLineOptionsAsync`: This method is called as last one and allows to do global coherency check. @@ -780,15 +887,15 @@ For example, let's say our testing framework has the capability to generate a te A possible implementation for the sample above could be: ```csharp - public Task ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) - { - bool generateReportEnabled = commandLineOptions.IsOptionSet(GenerateReportOption); - bool reportFileName = commandLineOptions.TryGetOptionArgumentList(ReportFilenameOption, out string[]? _); +public Task 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; - } + return (generateReportEnabled || reportFileName) && !(generateReportEnabled && reportFileName) + ? ValidationResult.InvalidTask("Both `--generatereport` and `--reportfilename` need to be provided simultaneously.") + : ValidationResult.ValidTask; +} ``` Please note that the `ValidateCommandLineOptionsAsync` method provides the [`ICommandLineOptions`](./unit-testing-platform-architecture-services.md#the-icommandlineoptions-service) service, which is used to fetch the argument information parsed by the platform itself. @@ -800,7 +907,7 @@ The `ITestSessionLifeTimeHandler` is an *in-process* extension that enables the To register a custom `ITestSessionLifeTimeHandler`, utilize the following API: ```csharp -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); +var builder = await TestApplication.CreateBuilderAsync(args); // ... @@ -818,13 +925,18 @@ The `ITestSessionLifeTimeHandler` interface includes the following methods: ```csharp public interface ITestSessionLifetimeHandler : ITestHostExtension { - Task OnTestSessionStartingAsync(SessionUid sessionUid, CancellationToken cancellationToken); - Task OnTestSessionFinishingAsync(SessionUid sessionUid, CancellationToken cancellationToken); + Task OnTestSessionStartingAsync( + SessionUid sessionUid, + CancellationToken cancellationToken); + + Task OnTestSessionFinishingAsync( + SessionUid sessionUid, + CancellationToken cancellationToken); } public readonly struct SessionUid(string value) { - public string Value { get; } + public string Value { get; } = value; } public interface ITestHostExtension : IExtension @@ -851,7 +963,7 @@ The `ITestApplicationLifecycleCallbacks` is an *in-process* extension that enabl To register a custom `ITestApplicationLifecycleCallbacks`, utilize the following api: ```csharp -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); +var builder = await TestApplication.CreateBuilderAsync(args); // ... @@ -871,7 +983,10 @@ The `ITestApplicationLifecycleCallbacks` interface includes the following method public interface ITestApplicationLifecycleCallbacks : ITestHostExtension { Task BeforeRunAsync(CancellationToken cancellationToken); - Task AfterRunAsync(int exitCode, CancellationToken cancellation); + + Task AfterRunAsync( + int exitCode, + CancellationToken cancellation); } public interface ITestHostExtension : IExtension @@ -898,7 +1013,7 @@ The `IDataConsumer` is an *in-process* extension capable of subscribing to and r To register a custom `IDataConsumer`, utilize the following api: ```csharp -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); +var builder = await TestApplication.CreateBuilderAsync(args); // ... @@ -917,7 +1032,11 @@ The `IDataConsumer` interface includes the following methods: public interface IDataConsumer : ITestHostExtension { Type[] DataTypesConsumed { get; } - Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken); + + Task ConsumeAsync( + IDataProducer dataProducer, + IData value, + CancellationToken cancellationToken); } public interface IData @@ -940,13 +1059,14 @@ internal class CustomDataConsumer : IDataConsumer, IOutputDeviceDataProducer { public Type[] DataTypesConsumed => new[] { typeof(TestNodeUpdateMessage) }; ... - public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + public Task ConsumeAsync( + IDataProducer dataProducer, + IData value, + CancellationToken cancellationToken) { var testNodeUpdateMessage = (TestNodeUpdateMessage)value; - TestNodeStateProperty nodeState = testNodeUpdateMessage.TestNode.Properties.Single(); - - switch (nodeState) + switch (testNodeUpdateMessage.TestNode.Properties.Single()) { case InProgressTestNodeStateProperty _: { @@ -996,7 +1116,7 @@ The `ITestHostEnvironmentVariableProvider` is an *out-of-process* extension that To register a custom `ITestHostEnvironmentVariableProvider`, utilize the following API: ```csharp -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); +var builder = await TestApplication.CreateBuilderAsync(args); // ... @@ -1015,7 +1135,9 @@ The `ITestHostEnvironmentVariableProvider` interface includes the following meth public interface ITestHostEnvironmentVariableProvider : ITestHostControllersExtension, IExtension { Task UpdateAsync(IEnvironmentVariables environmentVariables); - Task ValidateTestHostEnvironmentVariablesAsync(IReadOnlyEnvironmentVariables environmentVariables); + + Task ValidateTestHostEnvironmentVariablesAsync( + IReadOnlyEnvironmentVariables environmentVariables); } public interface IEnvironmentVariables : IReadOnlyEnvironmentVariables @@ -1026,13 +1148,21 @@ public interface IEnvironmentVariables : IReadOnlyEnvironmentVariables public interface IReadOnlyEnvironmentVariables { - bool TryGetVariable(string variable, [NotNullWhen(true)] out OwnedEnvironmentVariable? environmentVariable); + 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 OwnedEnvironmentVariable( + IExtension owner, + string variable, + string? value, + bool isSecret, + bool isLocked); } public class EnvironmentVariable @@ -1069,7 +1199,7 @@ The `ITestHostProcessLifetimeHandler` is an *out-of-process* extension that allo To register a custom `ITestHostProcessLifetimeHandler`, utilize the following API: ```csharp -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); +var builder = await TestApplication.CreateBuilderAsync(args); // ... @@ -1088,8 +1218,14 @@ The `ITestHostProcessLifetimeHandler` interface includes the following methods: public interface ITestHostProcessLifetimeHandler : ITestHostControllersExtension { Task BeforeTestHostProcessStartAsync(CancellationToken cancellationToken); - Task OnTestHostProcessStartedAsync(ITestHostProcessInformation testHostProcessInformation, CancellationToken cancellation); - Task OnTestHostProcessExitedAsync(ITestHostProcessInformation testHostProcessInformation, CancellationToken cancellation); + + Task OnTestHostProcessStartedAsync( + ITestHostProcessInformation testHostProcessInformation, + CancellationToken cancellation); + + Task OnTestHostProcessExitedAsync( + ITestHostProcessInformation testHostProcessInformation, + CancellationToken cancellation); } public interface ITestHostProcessInformation @@ -1140,6 +1276,8 @@ The testing platform consists of a [testing framework](#test-framework-extension ## Extensions helpers +The testing platform provides a set of helper classes and interfaces to simplify the implementation of extensions. These helpers are designed to streamline the development process and ensure that the extension adheres to the platform's standards. + ### Asynchronous initialization and cleanup of extensions The creation of the testing framework and extensions through factories adheres to the standard .NET object creation mechanism, which uses synchronous constructors. If an extension requires intensive initialization (such as accessing the file system or network), it cannot employ the *async/await* pattern in the constructor because constructors return void, not `Task`. @@ -1191,14 +1329,13 @@ internal class CustomExtension : ITestSessionLifetimeHandler, IDataConsumer, ... Once you've created the `CompositeExtensionFactory` for your type, you can register it with both the `IDataConsumer` and `ITestSessionLifetimeHandler` APIs, which offer an overload for the `CompositeExtensionFactory`: ```csharp -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args); +var builder = await TestApplication.CreateBuilderAsync(args); // ... -CompositeExtensionFactory factory = new(serviceProvider => new CustomExtension()); +var factory = new CompositeExtensionFactory(serviceProvider => new CustomExtension()); builder.TestHost.AddTestSessionLifetimeHandle(factory); - builder.TestHost.AddDataConsumer(factory); ``` diff --git a/docs/core/testing/unit-testing-platform-architecture-services.md b/docs/core/testing/unit-testing-platform-architecture-services.md index e597c9f6669bf..99af3bde0b5f8 100644 --- a/docs/core/testing/unit-testing-platform-architecture-services.md +++ b/docs/core/testing/unit-testing-platform-architecture-services.md @@ -27,14 +27,28 @@ The testing platform offers handy extension methods to access well-known service ```csharp public static class ServiceProviderExtensions { - public static TService GetRequiredService(this IServiceProvider provider) - public static TService? GetService(this IServiceProvider provider) - public static IMessageBus GetMessageBus(this IServiceProvider serviceProvider) - public static IConfiguration GetConfiguration(this IServiceProvider serviceProvider) - public static ICommandLineOptions GetCommandLineOptions(this IServiceProvider serviceProvider) - public static ILoggerFactory GetLoggerFactory(this IServiceProvider serviceProvider) - public static IOutputDevice GetOutputDevice(this IServiceProvider serviceProvider) - ...and more + public static TService GetRequiredService( + this IServiceProvider provider) + + public static TService? GetService( + this IServiceProvider provider) + + public static IMessageBus GetMessageBus( + this IServiceProvider serviceProvider) + + public static IConfiguration GetConfiguration( + this IServiceProvider serviceProvider) + + public static ICommandLineOptions GetCommandLineOptions( + this IServiceProvider serviceProvider) + + public static ILoggerFactory GetLoggerFactory( + this IServiceProvider serviceProvider) + + public static IOutputDevice GetOutputDevice( + this IServiceProvider serviceProvider) + + // ... and more } ``` @@ -83,7 +97,7 @@ The code snippet would look something like this: ```csharp IServiceProvider serviceProvider = null; // Get the service provider... -IConfiguration configuration = serviceProvider.GetConfiguration(); +var configuration = serviceProvider.GetConfiguration(); if (bool.TryParse(configuration["CustomTestingFramework:DisableParallelism"], out var value) && value is true) { @@ -109,7 +123,7 @@ The syntax to access to the fist element ("ThreadPool") is: ```csharp IServiceProvider serviceProvider = null; // Get the service provider... -IConfiguration configuration = serviceProvider.GetConfiguration(); +var configuration = serviceProvider.GetConfiguration(); var fistElement = configuration["CustomTestingFramework:Engine:0"]; ``` @@ -134,7 +148,7 @@ var options = new TestApplicationOptions(); options.Configuration.ConfigurationSources.RegisterEnvironmentVariablesConfigurationSource = false; -ITestApplicationBuilder builder = await TestApplication.CreateBuilderAsync(args, options); +var builder = await TestApplication.CreateBuilderAsync(args, options); ``` ## The `ICommandLineOptions` service @@ -147,7 +161,8 @@ public interface ICommandLineOptions bool IsOptionSet(string optionName); bool TryGetOptionArgumentList( - string optionName, out string[]? arguments); + string optionName, + out string[]? arguments); } ``` @@ -190,8 +205,18 @@ The logger factory allows you to create an `ILogger` object using the `CreateLog ```csharp public interface ILogger { - Task LogAsync(LogLevel logLevel, TState state, Exception? exception, Func formatter); - void Log(LogLevel logLevel, TState state, Exception? exception, Func formatter); + Task LogAsync( + LogLevel logLevel, + TState state, + Exception? exception, + Func formatter); + + void Log( + LogLevel logLevel, + TState state, + Exception? exception, + Func formatter); + bool IsEnabled(LogLevel logLevel); } @@ -201,22 +226,22 @@ public interface ILogger : ILogger public static class LoggingExtensions { - public static Task LogTraceAsync(this ILogger logger, string message); + public static Task LogCriticalAsync(this ILogger logger, string message); public static Task LogDebugAsync(this ILogger logger, string message); + public static Task LogErrorAsync(this ILogger logger, Exception ex); + public static Task LogErrorAsync(this ILogger logger, string message, Exception ex); + public static Task LogErrorAsync(this ILogger logger, string message); public static Task LogInformationAsync(this ILogger logger, string message); + public static Task LogTraceAsync(this ILogger logger, string message); public static Task LogWarningAsync(this ILogger logger, string message); - public static Task LogErrorAsync(this ILogger logger, string message); - public static Task LogErrorAsync(this ILogger logger, string message, Exception ex); - public static Task LogErrorAsync(this ILogger logger, Exception ex); - public static Task LogCriticalAsync(this ILogger logger, string message); - public static void LogTrace(this ILogger logger, string message); + public static void LogCritical(this ILogger logger, string message); public static void LogDebug(this ILogger logger, string message); + public static void LogError(this ILogger logger, Exception ex); + public static void LogError(this ILogger logger, string message, Exception ex); + public static void LogError(this ILogger logger, string message); public static void LogInformation(this ILogger logger, string message); + public static void LogTrace(this ILogger logger, string message); public static void LogWarning(this ILogger logger, string message); - public static void LogError(this ILogger logger, string message); - public static void LogError(this ILogger logger, string message, Exception ex); - public static void LogError(this ILogger logger, Exception ex); - public static void LogCritical(this ILogger logger, string message); } ``` @@ -231,7 +256,7 @@ public enum LogLevel Warning, Error, Critical, - None + None, } ``` @@ -241,9 +266,9 @@ Here's an example of how you might use the logging API: ... IServiceProvider provider = null; // Get the service provider... -ILoggerFactory factory = provider.GetLoggerFactory(); +var factory = provider.GetLoggerFactory(); -ILogger logger = factory.CreateLogger(); +var logger = factory.CreateLogger(); // ... @@ -275,7 +300,9 @@ The `IMessageBus` satisfied the *pushing action* to the bus and the API is: ```csharp public interface IMessageBus { - Task PublishAsync(IDataProducer dataProducer, IData data); + Task PublishAsync( + IDataProducer dataProducer, + IData data); } public interface IDataProducer : IExtension @@ -319,7 +346,9 @@ The API consists of: ```csharp public interface IOutputDevice { - Task DisplayAsync(IOutputDeviceDataProducer producer, IOutputDeviceData data); + Task DisplayAsync( + IOutputDeviceDataProducer producer, + IOutputDeviceData data); } public interface IOutputDeviceDataProducer : IExtension @@ -362,7 +391,7 @@ Here's an example of how you might use the colored text with the *active* output ```csharp IServiceProvider provider = null; // Get the service provider... -IOutputDevice outputDevice = provider.GetOutputDevice(); +var outputDevice = provider.GetOutputDevice(); await outputDevice.DisplayAsync( this, From 5055dc9a09dfbeebb68a4e082ab3fe1d42f72af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 9 Aug 2024 15:26:14 +0200 Subject: [PATCH 19/19] Update message bus --- docs/core/testing/media/bus.png | Bin 51343 -> 0 bytes docs/core/testing/media/message-bus.png | Bin 0 -> 35263 bytes ...it-testing-platform-architecture-services.md | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 docs/core/testing/media/bus.png create mode 100644 docs/core/testing/media/message-bus.png diff --git a/docs/core/testing/media/bus.png b/docs/core/testing/media/bus.png deleted file mode 100644 index b325c10bbfa41ff7d65dd4ad36363a01a666718d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51343 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 diff --git a/docs/core/testing/media/message-bus.png b/docs/core/testing/media/message-bus.png new file mode 100644 index 0000000000000000000000000000000000000000..d26a890eff3b6532d9defb2e8ccf030a5d7bb125 GIT binary patch literal 35263 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

}76>DKA%qmD6iwCyX_NRBWH!{RM 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 VL`#QNAHx8TytJ}Zm4tEd{{