From ed36ed3ad0620a231f5df79e5407451735fab4a2 Mon Sep 17 00:00:00 2001 From: Eric Sibly Date: Sun, 29 Mar 2026 11:41:02 -0700 Subject: [PATCH 1/5] ## v5.10.0 - *Enhancement:* `TestSharedState` updated to enable request-based (isolated) state; with the corresponding `GetHttpRequestId()` method now made public. - *Enhancement:* Added `ApiTesterBase.AddPreRunAction()`, `AddPostRunBeforeExpectationsAction()`, `AddPostRunAfterExpectationsAction()` and `AddPostRunAction()` to enable the addition of actions to be executed at different stages of the `Run`/`RunAsync` process for every underlying test. This will enable the addition of common pre/post processing logic without having to explicitly add to each test. - *Enhancement:* `TEntryPoint` methods can now be specified as `static`. - *Enhancement:* `ApiError` added implicit conversion from (`string?`, `string`) tuple to enable simplified usage; e.g. `AssertErrors(("field", "Bad Request"))` versus `AssertErrors(new ApiError("field", "Bad Request"))`. - *Fixed:* Reset `MockHttpClientRequest` internal state when using any `With*` method to ensure correct behavior. --- CHANGELOG.md | 27 ++- Common.targets | 2 +- .../ExtensionMethods.cs | 4 +- .../Abstractions/TestSharedState.cs | 34 +++- src/UnitTestEx/Abstractions/TesterBase.cs | 96 +++++++-- src/UnitTestEx/ApiError.cs | 12 ++ src/UnitTestEx/AspNetCore/ApiTesterBase.cs | 54 +++++ src/UnitTestEx/AspNetCore/HttpTesterBase.cs | 80 +++++--- .../Assertors/ActionResultAssertor.cs | 6 +- src/UnitTestEx/Assertors/Assertor.cs | 25 ++- .../HttpResponseMessageAssertorBase.cs | 2 +- .../HttpResponseMessageAssertorBaseT.cs | 6 +- src/UnitTestEx/Assertors/ValueAssertor.cs | 4 +- .../Expectations/ExpectationsArranger.cs | 2 +- .../Expectations/ExpectationsExtensions.cs | 2 +- src/UnitTestEx/Expectations/IExpectations.cs | 7 +- src/UnitTestEx/Hosting/EntryPoint.cs | 12 +- src/UnitTestEx/Hosting/ScopedTypeTester.cs | 188 +++++++++++------- src/UnitTestEx/Mocking/MockHttpClient.cs | 4 +- .../Mocking/MockHttpClientRequest.cs | 27 ++- .../Mocking/MockHttpClientRequestRule.cs | 12 ++ .../Mocking/MockHttpClientResponse.cs | 10 +- src/UnitTestEx/ObjectComparer.cs | 4 +- src/UnitTestEx/Resource.cs | 8 +- src/UnitTestEx/TestSetUp.cs | 4 +- src/UnitTestEx/TesterArgs.cs | 16 ++ .../MockHttpClientTest.cs | 40 +++- .../Other/GenericTest.cs | 2 +- .../Other/ObjectComparerTest.cs | 9 + .../PersonControllerTest.cs | 6 +- 30 files changed, 536 insertions(+), 169 deletions(-) create mode 100644 src/UnitTestEx/TesterArgs.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index f86c03a..c5aceeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,15 @@ Represents the **NuGet** versions. +## v5.10.0 +- *Enhancement:* `TestSharedState` updated to enable request-based (isolated) state; with the corresponding `GetHttpRequestId()` method now made public. +- *Enhancement:* Added `ApiTesterBase.AddPreRunAction()`, `AddPostRunBeforeExpectationsAction()`, `AddPostRunAfterExpectationsAction()` and `AddPostRunAction()` to enable the addition of actions to be executed at different stages of the `Run`/`RunAsync` process for every underlying test. This will enable the addition of common pre/post processing logic without having to explicitly add to each test. +- *Enhancement:* `TEntryPoint` methods can now be specified as `static`. +- *Enhancement:* `ApiError` added implicit conversion from (`string?`, `string`) tuple to enable simplified usage; e.g. `AssertErrors(("field", "Bad Request"))` versus `AssertErrors(new ApiError("field", "Bad Request"))`. +- *Fixed:* Reset `MockHttpClientRequest` internal state when using any `With*` method to ensure correct behavior. + ## v5.9.2 -- *Fixed:* The `MockHttpClientRequest` now resets the internal count state when overridding a response to ensure `Verify()` functions correctly. +- *Fixed:* The `MockHttpClientRequest` now resets the internal count state when overriding a response to ensure `Verify()` functions correctly. ## v5.9.1 - *Fixed:* The `MockHttpClientRequest` now caches the response content internally, and creates a new `HttpContent` instance for each request to ensure that the content can be read multiple times across multiple requests (where applicable); avoids potential object disposed error. @@ -95,10 +102,10 @@ Represents the **NuGet** versions. - `CreateFunctionTester()` replaced with `FunctionTester.Create()`. ## v4.4.2 -- *Fixed*: Updated `System.Text.Json` package depenedency to latest; resolve [Microsoft Security Advisory CVE-2024-43485](https://github.com/advisories/GHSA-8g4q-xg66-9fp4). +- *Fixed*: Updated `System.Text.Json` package dependency to latest; resolve [Microsoft Security Advisory CVE-2024-43485](https://github.com/advisories/GHSA-8g4q-xg66-9fp4). ## v4.4.1 -- *Fixed:* Updated all package depenedencies to latest. +- *Fixed:* Updated all package dependencies to latest. ## v4.4.0 - *Enhancement:* Added `ExpectJson` and `ExpectJsonFromResource` to `IValueExpectations` to enable value comparison against the specified (expected) JSON. @@ -136,7 +143,7 @@ Represents the **NuGet** versions. - *Fixed:* Removed all dependencies to `Newtonsoft.Json`; a developer will need to explicitly add this dependency and `IJsonSerializer` implementation where applicable. ## v4.0.0 -All internal dependecies to `CoreEx` have been removed. This is intended to further generalize the capabilities of `UnitTestEx`; but more importantly, break the circular dependency reference between the two repositories. New `CoreEx.UnitTesting*` packages have been created to extend the `UnitTestEx` capabilities for `CoreEx`. +All internal dependencies to `CoreEx` have been removed. This is intended to further generalize the capabilities of `UnitTestEx`; but more importantly, break the circular dependency reference between the two repositories. New `CoreEx.UnitTesting*` packages have been created to extend the `UnitTestEx` capabilities for `CoreEx`. - *Enhancement:* All typed value assertions have been named `AssertValue` for consistency; otherwise, `AssertContent` for a simple string comparison. - *Enhancement:* All JSON-related assertions have been named `AssertJson*` for consistency. - *Enhancement:* The `CreateServiceBusMessage` methods that accept a generic `T` value have been renamed to `CreateServiceBusMessageFromValue`. @@ -149,7 +156,7 @@ All internal dependecies to `CoreEx` have been removed. This is intended to furt The enhancements have been made in a manner to maximize backwards compatibility with previous versions of `UnitTestEx` where possible; however, some breaking changes were unfortunately unavoidable (and made to improve overall). There may be an opportunity for the consuming developer to add extension methods to support the previous naming conventions if desired; note that the next `CoreEx` version (`v3.6.0`) will implement extensions in new `CoreEx.UnitTesting` packages to support existing behaviors (where applicable). ## v3.1.0 -- *Enhancement:* Updated all package depenedencies to latest. +- *Enhancement:* Updated all package dependencies to latest. - *Enhancement:* The `GenericTester` updated to support `IHostStartup` to enable shared host dependency injection configuration. - *Enhancement:* Added `IEventExpectations` and `ILoggerExpectations` support to `GenericTester` and `ValidationTester`. - *Enhancement:* Moved the `CreateServiceBusMessage` and related methods from `FunctionTesterBase` up the inheritance hierarchy to `TesterBase` to enable broader usage. @@ -193,7 +200,7 @@ The enhancements have been made in a manner to maximize backwards compatibility - *Fixed:* Incorrect package deployment; corrected. ## v2.1.0 -- *Enhancement:* Added `TestSetUp.RegisterAutoSetUp` that will automatically throw a `TestSetUpException` where unsuccessful; otherwise, queues the successful output message. This is required as most testing frameworks do not allow for a log write during construction or fuxture set up. The underlying _UnitTestEx_ test classes will automatically log write where entries are discovered in the queue. This will at least ensure one of the underlying tests will output the success output where applicable. Otherwise, to log write explicitly use `TestSetUp.LogAutoSetUpOutputs`. +- *Enhancement:* Added `TestSetUp.RegisterAutoSetUp` that will automatically throw a `TestSetUpException` where unsuccessful; otherwise, queues the successful output message. This is required as most testing frameworks do not allow for a log write during construction or fixture set up. The underlying _UnitTestEx_ test classes will automatically log write where entries are discovered in the queue. This will at least ensure one of the underlying tests will output the success output where applicable. Otherwise, to log write explicitly use `TestSetUp.LogAutoSetUpOutputs`. ## v2.0.0 - *Enhancement:* Updated `CoreEx` dependencies to `2.0.0` as breaking changes were introduced. There are no breaking changes within `UnitTestEx` as a result; primarily related to the key `CoreEx` dependency. @@ -224,7 +231,7 @@ The enhancements have been made in a manner to maximize backwards compatibility - *Fixed:* Handle `AggregateException` by using its `InnerException` as the `Exception`. ## v1.0.24 -- *Enhancement:* Updated the `ControllerTester` removing the HTTP request capabilies and moving into new `HttpTester`; this creates a more logical split as the latter needs no knowledge of the `Controller`. This new tester is available via `ApiTester.Http().Run(...)`. +- *Enhancement:* Updated the `ControllerTester` removing the HTTP request capabilities and moving into new `HttpTester`; this creates a more logical split as the latter needs no knowledge of the `Controller`. This new tester is available via `ApiTester.Http().Run(...)`. - *Enhancement:* Added new `UnitTextEx.Expectations` namespace; when added will enable `Expect*` and `Ignore*` pre-execution assertions, that are then executed post `Run`/`RunAsync`. Some testers now also support the specification of a `TResponse` generic `Type` to enable further response value-related expectations. ## v1.0.23 @@ -283,7 +290,7 @@ The enhancements have been made in a manner to maximize backwards compatibility - *Enhancement:* **Breaking change.** Integrate [`CoreEx`](https://github.com/Avanade/CoreEx/) package which primarily brings [`IJsonSerializer`](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx/Json/IJsonSerializer.cs) functionality to enable configuration of either [`CoreEx.Text.Json.JsonSerializer`](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx/Text/Json/JsonSerializer.cs) (default) or [`CoreEx.Newtonsoft.Json.JsonSerializer`](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Newtonsoft/Json/JsonSerializer.cs). The `MockHttpClientFactory`, `ApiTester` and `FunctionTester` have new method `UseJsonSerializer` to individually update from the default. To change the default for all tests then set [`CoreEx.Json.JsonSerializer.Default`](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx/Json/JsonSerializer.cs) to the desired serializer. - *Enhancement:* Improved the replacement of the `MockHttpClientFactory` with the `ApiTester` and `FunctionTester`. Existing code `test.ConfigureServices(sc => mcf.Replace(sc))` can be replaced with `test.ReplaceHttpClientFactory(mcf)`. - *Enhancement:* Added `ReplaceSingleton`, `ReplaceScoped` and `ReplaceTransient` methods directly to `ApiTester` and `FunctionTester`. For example, existing code `test.ConfigureServices(sc => sc.ReplaceTransient())` can be replaced with `test.ReplaceTransient()`. -- *Enhancement:* Added addtional `CreateHttpRequest` overloads to support additional parameters `HttpRequestOptions? requestOptions = null, params IHttpArg[] args` as enabled by `CoreEx`. These enable additional capabilities for the `HttpRequest` query string and headers. +- *Enhancement:* Added additional `CreateHttpRequest` overloads to support additional parameters `HttpRequestOptions? requestOptions = null, params IHttpArg[] args` as enabled by `CoreEx`. These enable additional capabilities for the `HttpRequest` query string and headers. ## v1.0.11 - *[Issue 24](https://github.com/Avanade/UnitTestEx/issues/24)*: Added additional `IServiceCollection.Replace` extension methods to support `ReplaceXxx()` and `ReplaceXxx()` to match the standard `AddXxx` methods. @@ -319,10 +326,10 @@ The enhancements have been made in a manner to maximize backwards compatibility ## v1.0.4 - *[Issue 3](https://github.com/Avanade/UnitTestEx/issues/3)*: Added support for MOQ `Times` struct to verify the number of times a request is made. - *[Issue 4](https://github.com/Avanade/UnitTestEx/issues/4)*: Added support for MOQ sequences; i.e. multiple different responses. -- *[Issue 5](https://github.com/Avanade/UnitTestEx/issues/5)*: Deleted `MockServiceBus` as the mocking failed to work as intended. This has been replaced by `FunctionTesterBase` methods of `CreateServiceBusMessage`, `CreateServiceBusMessageFromResource` and `CreateServiceBusMessageFromJson`. +- *[Issue 5](https://github.com/Avanade/UnitTestEx/issues/5)*: Delete `MockServiceBus` as the mocking failed to work as intended. This has been replaced by `FunctionTesterBase` methods of `CreateServiceBusMessage`, `CreateServiceBusMessageFromResource` and `CreateServiceBusMessageFromJson`. ## v1.0.3 -- *Fixed:* `MockHttpClientFactory.CreateClient` overloads were ambiquous, this has been corrected. +- *Fixed:* `MockHttpClientFactory.CreateClient` overloads were ambiguous, this has been corrected. - *Fixed:* Resolved logging output challenges between the various test frameworks and `ApiTester` (specifically) to achieve consistent output. - *Enhancement:* The logging output now includes scope details. - *Added:* New `MockServiceBus.CreateReceivedMessage` which will mock the `ServiceBusReceivedMessage` and add the passed value as serialized JSON into the `Body` (`BinaryData`). diff --git a/Common.targets b/Common.targets index 36c86d4..8dd083e 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 5.9.2 + 5.10.0 preview Avanade Avanade diff --git a/src/UnitTestEx.Azure.ServiceBus/ExtensionMethods.cs b/src/UnitTestEx.Azure.ServiceBus/ExtensionMethods.cs index 2bbbb92..40b4788 100644 --- a/src/UnitTestEx.Azure.ServiceBus/ExtensionMethods.cs +++ b/src/UnitTestEx.Azure.ServiceBus/ExtensionMethods.cs @@ -41,7 +41,7 @@ public static ServiceBusReceivedMessage CreateServiceBusMessageFromValue(this /// /// The to infer for the embedded resources. /// The . - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// Optional modifier than enables the message to be further configured. /// The . public static ServiceBusReceivedMessage CreateServiceBusMessageFromResource(this TesterBase tester, string resourceName, Action? messageModify = null) @@ -51,7 +51,7 @@ public static ServiceBusReceivedMessage CreateServiceBusMessageFromResource where the will contain the JSON formatted embedded resource as the content (). /// /// The . - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// Optional modifier than enables the message to be further configured. /// The that contains the embedded resource; defaults to . /// The . diff --git a/src/UnitTestEx/Abstractions/TestSharedState.cs b/src/UnitTestEx/Abstractions/TestSharedState.cs index 2d4921a..7ce2678 100644 --- a/src/UnitTestEx/Abstractions/TestSharedState.cs +++ b/src/UnitTestEx/Abstractions/TestSharedState.cs @@ -13,6 +13,9 @@ namespace UnitTestEx.Abstractions /// /// Provides a means to share state between the and the corresponding execution. /// + /// The -based functionality is primarily intended for use with and related HTTP testing; however, it is available for any + /// testing where sharing state between the tester and execution is required. + /// Be careful when using this class that data does not cross boundaries where it is scoped, or may be disposed, as this may result in unintended side-effect/consequences. public sealed class TestSharedState { #if NET9_0_OR_GREATER @@ -38,7 +41,7 @@ internal TestSharedState() { } /// The log message. public void AddLoggerMessage(string? message) { - var id = GetRequestId(); + var id = GetHttpRequestId(); lock (_lock) { @@ -54,9 +57,11 @@ public void AddLoggerMessage(string? message) } /// - /// Gets the correlation identifier. + /// Gets the HTTP request correlation identifier. /// - private string GetRequestId() + /// This identifier is used to correlate log messages and other state information with a specific HTTP request. + /// This is only meaningful within the context of an executing host. + public string GetHttpRequestId() { if (HttpContextAccessor == null || HttpContextAccessor.HttpContext == null) return string.Empty; @@ -94,6 +99,29 @@ private string GetRequestId() /// public ConcurrentDictionary StateData { get; } = new ConcurrentDictionary(); + /// + /// Gets the state extension data for the specified that can be used for additional state information (where applicable). + /// + /// The unit testing request identifier. + /// The state extension data for the specified . + /// A that is will return the ; i.e. is assumed not to be request-based. + public ConcurrentDictionary RequestStateData(string? requestId) + => string.IsNullOrEmpty(requestId) + ? StateData + : StateData.GetOrAdd(requestId, _ => new ConcurrentDictionary()) as ConcurrentDictionary ?? new ConcurrentDictionary(); + + /// + /// Removes the state data associated with the specified , if it exists. + /// + /// The unit testing request identifier. + public void RemoveRequestStateData(string? requestId) + { + if (string.IsNullOrEmpty(requestId)) + return; + + StateData.TryRemove(requestId, out _); + } + /// /// Resets the . /// diff --git a/src/UnitTestEx/Abstractions/TesterBase.cs b/src/UnitTestEx/Abstractions/TesterBase.cs index e9144c9..bec904c 100644 --- a/src/UnitTestEx/Abstractions/TesterBase.cs +++ b/src/UnitTestEx/Abstractions/TesterBase.cs @@ -15,6 +15,8 @@ using System.Reflection; using System.Text; using System.Threading; +using UnitTestEx.AspNetCore; +using UnitTestEx.Expectations; using UnitTestEx.Json; using UnitTestEx.Logging; @@ -190,7 +192,7 @@ public void ResetHost(bool resetConfiguredServices = false) /// /// Enables opportunity to execute logic immediately after the underlying host has been started. /// - /// Where overridding ensure the base is invoked first to avoid unintended side-effects as will invoke the registered . + /// Where overriding ensure the base is invoked first to avoid unintended side-effects as will invoke the registered . /// Note: a host lifetime can span one or more tests so this should not be used for per-test set-up/configuration. Equally, a will result in a new host instantiation on first access. protected virtual void OnHostStartUp() { @@ -253,6 +255,66 @@ protected void AddConfiguredServices(IServiceCollection services) IsHostInstantiated = true; } + /// + /// Gets the list of pre-run actions to be executed before the underlying test Run occurs. + /// + protected List> PreRunActions { get; } = []; + + /// + /// Gets the list of post-run actions to be executed after the underlying test Run occurs (before ). + /// + protected List> PostRunBeforeExpectationsActions { get; } = []; + + /// + /// Gets the list of post-run actions to be executed after the underlying test Run occurs (after ). + /// + protected List> PostRunAfterExpectationsActions { get; } = []; + + /// + /// Gets the list of post-run actions to be executed after the underlying test Run occurs (always executed regardless of result to enable the likes of clean-up etc.). + /// + protected List> PostRunActions { get; } = []; + + /// + /// Executes the pre-run actions before the underlying test Run occurs. + /// + /// The tester instance. + internal void ExecutePreRunActions(IExpectations tester) + { + foreach (var action in PreRunActions) + action(tester); + } + + /// + /// Executes the post-run actions after the underlying test Run occurs (before ). + /// + /// The tester instance. + internal void ExecutePostRunBeforeExpectationsActions(IExpectations tester) + { + foreach (var action in PostRunBeforeExpectationsActions) + action(tester); + } + + /// + /// Executes the post-run actions after the underlying test Run occurs (before ). + /// + /// The tester instance. + internal void ExecutePostRunAfterExpectationsActions(IExpectations tester) + { + foreach (var action in PostRunAfterExpectationsActions) + action(tester); + } + + /// + /// Executes the post-run actions after the underlying test Run occurs (always executed regardless of result to enable the likes of clean-up etc.). + /// + /// The tester instance. + internal void ExecutePostRunActions(IExpectations tester) + { + foreach (var action in PostRunActions) + action(tester); + } + /// /// Replaces the with the specified . /// @@ -311,7 +373,7 @@ internal void LogHttpResponseMessage(HttpResponseMessage res, Stopwatch? sw) /// Creates a new with no body. /// /// The . - /// The requuest uri. + /// The request uri. /// The . #if NET7_0_OR_GREATER public HttpRequest CreateHttpRequest(HttpMethod httpMethod, [StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri) @@ -324,7 +386,7 @@ public HttpRequest CreateHttpRequest(HttpMethod httpMethod, string? requestUri) /// Creates a new with (defaults to ). /// /// The . - /// The requuest uri. + /// The request uri. /// The optional body content. /// The . #if NET7_0_OR_GREATER @@ -338,7 +400,7 @@ public HttpRequest CreateHttpRequest(HttpMethod httpMethod, string? requestUri, /// Creates a new with and . /// /// The . - /// The requuest uri. + /// The request uri. /// The optional body content. /// The content type. Defaults to . /// The . @@ -353,7 +415,7 @@ public HttpRequest CreateHttpRequest(HttpMethod httpMethod, string? requestUri, /// Creates a new with no body. /// /// The . - /// The requuest uri. + /// The request uri. /// The optional modifier. /// The . #if NET7_0_OR_GREATER @@ -367,7 +429,7 @@ public HttpRequest CreateHttpRequest(HttpMethod httpMethod, string? requestUri, /// Creates a new with (defaults to ). /// /// The . - /// The requuest uri. + /// The request uri. /// The optional body content. /// The optional modifier. /// The . @@ -382,7 +444,7 @@ public HttpRequest CreateHttpRequest(HttpMethod httpMethod, string? requestUri, /// Creates a new with optional (defaults to ). /// /// The . - /// The requuest uri. + /// The request uri. /// The optional body content. /// The content type. Defaults to . /// The optional modifier. @@ -432,7 +494,7 @@ public HttpRequest CreateHttpRequest(HttpMethod httpMethod, string? requestUri = /// Creates a new with the JSON serialized as of . /// /// The . - /// The requuest uri. + /// The request uri. /// The value to JSON serialize. /// The . #if NET7_0_OR_GREATER @@ -446,7 +508,7 @@ public HttpRequest CreateJsonHttpRequest(HttpMethod httpMethod, string? requestU /// Creates a new with the JSON serialized as of . /// /// The . - /// The requuest uri. + /// The request uri. /// The value to JSON serialize. /// The optional modifier. /// The . @@ -462,8 +524,8 @@ public HttpRequest CreateJsonHttpRequest(HttpMethod httpMethod, string? requestU /// /// The to infer for the embedded resources. /// The . - /// The requuest uri. - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The request uri. + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The . #if NET7_0_OR_GREATER public HttpRequest CreateJsonHttpRequestFromResource(HttpMethod httpMethod, [StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, string resourceName) @@ -477,8 +539,8 @@ public HttpRequest CreateJsonHttpRequestFromResource(HttpMethod httpM /// /// The to infer for the embedded resources. /// The . - /// The requuest uri. - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The request uri. + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The optional modifier. /// The . #if NET7_0_OR_GREATER @@ -492,8 +554,8 @@ public HttpRequest CreateJsonHttpRequestFromResource(HttpMethod httpM /// Creates a new using the JSON formatted embedded resource as the content (). /// /// The . - /// The requuest uri. - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The request uri. + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The that contains the embedded resource; defaults to . /// The . #if NET7_0_OR_GREATER @@ -507,8 +569,8 @@ public HttpRequest CreateJsonHttpRequestFromResource(HttpMethod httpMethod, stri /// Creates a new using the JSON formatted embedded resource as the content (). /// /// The . - /// The requuest uri. - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The request uri. + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The that contains the embedded resource; defaults to . /// The optional modifier. /// The . diff --git a/src/UnitTestEx/ApiError.cs b/src/UnitTestEx/ApiError.cs index bc22b64..5c9afe0 100644 --- a/src/UnitTestEx/ApiError.cs +++ b/src/UnitTestEx/ApiError.cs @@ -18,5 +18,17 @@ public class ApiError(string? field, string message) /// Gets the error message. /// public string Message { get; } = message; + + /// + /// Implicitly converts a (string? field, string message) tuple to an . + /// + /// The tuple containing the field and message. + public static implicit operator ApiError((string? field, string message) error) => new(error.field, error.message); + + /// + /// Implicitly converts a to an . + /// + /// The error message. + public static implicit operator ApiError(string message) => new(null, message); } } \ No newline at end of file diff --git a/src/UnitTestEx/AspNetCore/ApiTesterBase.cs b/src/UnitTestEx/AspNetCore/ApiTesterBase.cs index fa0161d..c039f2c 100644 --- a/src/UnitTestEx/AspNetCore/ApiTesterBase.cs +++ b/src/UnitTestEx/AspNetCore/ApiTesterBase.cs @@ -10,10 +10,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading.Tasks; using UnitTestEx.Abstractions; +using UnitTestEx.Expectations; namespace UnitTestEx.AspNetCore { @@ -181,6 +183,58 @@ public TSelf UseSolutionRelativeContentRoot(string? solutionRelativePath) return (TSelf)this; } + /// + /// Adds an action to be executed before the underlying test Run occurs. + /// + /// The action to execute. + /// The to support fluent-style method-chaining. + public TSelf AddPreRunAction(Action action) + { + if (action is not null) + PreRunActions.Add(tester => action(tester)); + + return (TSelf)this; + } + + /// + /// Adds an action to be to be executed after the underlying test Run occurs (before ). + /// + /// The action to execute. + /// The to support fluent-style method-chaining. + public TSelf AddPostRunBeforeExpectationsAction(Action action) + { + if (action is not null) + PostRunBeforeExpectationsActions.Add(tester => action(tester)); + + return (TSelf)this; + } + + /// + /// Adds an action to be to be executed after the underlying test Run occurs (after ). + /// + /// The action to execute. + /// The to support fluent-style method-chaining. + public TSelf AddPostRunAfterExpectationsAction(Action action) + { + if (action is not null) + PostRunAfterExpectationsActions.Add(tester => action(tester)); + + return (TSelf)this; + } + + /// + /// Adds an action to be executed after the underlying test Run occurs (after all other post-run actions regardless of result). + /// + /// The action to execute. + /// The to support fluent-style method-chaining. + public TSelf AddPostRunAction(Action action) + { + if (action is not null) + PostRunActions.Add(tester => action(tester)); + + return (TSelf)this; + } + /// /// Releases all resources. /// diff --git a/src/UnitTestEx/AspNetCore/HttpTesterBase.cs b/src/UnitTestEx/AspNetCore/HttpTesterBase.cs index 863a0dc..3c119e9 100644 --- a/src/UnitTestEx/AspNetCore/HttpTesterBase.cs +++ b/src/UnitTestEx/AspNetCore/HttpTesterBase.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using UnitTestEx.Abstractions; using UnitTestEx.Assertors; +using UnitTestEx.Expectations; using UnitTestEx.Json; using UnitTestEx.Mocking; @@ -22,7 +23,7 @@ namespace UnitTestEx.AspNetCore /// /// Provides the base HTTP testing capabilities. /// - public abstract class HttpTesterBase + public abstract class HttpTesterBase : IExpectations { /// /// Gets the 'unit-test-ex-request-id' constant. @@ -67,7 +68,7 @@ public HttpTesterBase(TesterBase owner, TestServer testServer) public string? UserName { get; protected set; } /// - /// Gets the unqiue request identifier. + /// Gets the unique request identifier. /// /// This value is related to the . public string RequestId { get; } = Guid.NewGuid().ToString(); @@ -90,11 +91,22 @@ protected async Task SendAsync(HttpMethod httpMetho protected async Task SendAsync(HttpMethod httpMethod, string? requestUri, Action? requestModifier) #endif { - using var client = CreateHttpClient(); - var res = await new TypedHttpClient(client, JsonSerializer).SendAsync(httpMethod, requestUri, requestModifier).ConfigureAwait(false); - await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); - await AssertExpectationsAsync(res).ConfigureAwait(false); - return new HttpResponseMessageAssertor(Owner, res); + try + { + Owner.ExecutePreRunActions(this); + using var client = CreateHttpClient(); + var res = await new TypedHttpClient(client, JsonSerializer).SendAsync(httpMethod, requestUri, requestModifier).ConfigureAwait(false); + await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); + Owner.ExecutePostRunBeforeExpectationsActions(this); + await AssertExpectationsAsync(res).ConfigureAwait(false); + Owner.ExecutePostRunAfterExpectationsActions(this); + return new HttpResponseMessageAssertor(Owner, res); + } + finally + { + Owner.ExecutePostRunActions(this); + Owner.SharedState.RemoveRequestStateData(RequestId); + } } /// @@ -112,14 +124,25 @@ protected async Task SendAsync(HttpMethod httpMetho protected async Task SendAsync(HttpMethod httpMethod, string? requestUri, string? content, string? contentType, Action? requestModifier) #endif { - if (content != null && httpMethod == HttpMethod.Get) - Owner.LoggerProvider.CreateLogger("ApiTester").LogWarning("A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request (see https://www.rfc-editor.org/rfc/rfc7231)."); - - using var client = CreateHttpClient(); - var res = await new TypedHttpClient(client, JsonSerializer).SendAsync(httpMethod, requestUri, content, contentType, requestModifier).ConfigureAwait(false); - await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); - await AssertExpectationsAsync(res).ConfigureAwait(false); - return new HttpResponseMessageAssertor(Owner, res); + try + { + Owner.ExecutePreRunActions(this); + if (content != null && httpMethod == HttpMethod.Get) + Owner.LoggerProvider.CreateLogger("ApiTester").LogWarning("A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request (see https://www.rfc-editor.org/rfc/rfc7231)."); + + using var client = CreateHttpClient(); + var res = await new TypedHttpClient(client, JsonSerializer).SendAsync(httpMethod, requestUri, content, contentType, requestModifier).ConfigureAwait(false); + await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); + Owner.ExecutePostRunBeforeExpectationsActions(this); + await AssertExpectationsAsync(res).ConfigureAwait(false); + Owner.ExecutePostRunAfterExpectationsActions(this); + return new HttpResponseMessageAssertor(Owner, res); + } + finally + { + Owner.ExecutePostRunActions(this); + Owner.SharedState.RemoveRequestStateData(RequestId); + } } /// @@ -136,14 +159,25 @@ protected async Task SendAsync(HttpMethod httpMetho protected async Task SendAsync(HttpMethod httpMethod, string? requestUri, object? value, Action? requestModifier) #endif { - if (value != null && httpMethod == HttpMethod.Get) - Owner.LoggerProvider.CreateLogger("ApiTester").LogWarning("A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request (see https://www.rfc-editor.org/rfc/rfc7231)."); - - using var client = CreateHttpClient(); - var res = await new TypedHttpClient(client, JsonSerializer).SendAsync(httpMethod, requestUri, value, requestModifier).ConfigureAwait(false); - await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); - await AssertExpectationsAsync(res).ConfigureAwait(false); - return new HttpResponseMessageAssertor(Owner, res); + try + { + Owner.ExecutePreRunActions(this); + if (value != null && httpMethod == HttpMethod.Get) + Owner.LoggerProvider.CreateLogger("ApiTester").LogWarning("A payload within a GET request message has no defined semantics; sending a payload body on a GET request might cause some existing implementations to reject the request (see https://www.rfc-editor.org/rfc/rfc7231)."); + + using var client = CreateHttpClient(); + var res = await new TypedHttpClient(client, JsonSerializer).SendAsync(httpMethod, requestUri, value, requestModifier).ConfigureAwait(false); + await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); + Owner.ExecutePostRunBeforeExpectationsActions(this); + await AssertExpectationsAsync(res).ConfigureAwait(false); + Owner.ExecutePostRunAfterExpectationsActions(this); + return new HttpResponseMessageAssertor(Owner, res); + } + finally + { + Owner.ExecutePreRunActions(this); + Owner.SharedState.RemoveRequestStateData(RequestId); + } } /// diff --git a/src/UnitTestEx/Assertors/ActionResultAssertor.cs b/src/UnitTestEx/Assertors/ActionResultAssertor.cs index a2f408d..ba86749 100644 --- a/src/UnitTestEx/Assertors/ActionResultAssertor.cs +++ b/src/UnitTestEx/Assertors/ActionResultAssertor.cs @@ -118,7 +118,7 @@ public ActionResultAssertor AssertValue(TValue expectedValue, params str /// Asserts that the has the specified Content matches the JSON serialized value. /// /// The value . - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected value as serialized JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected value as serialized JSON. /// The JSON paths to ignore from the comparison. /// The to support fluent-style method-chaining. public ActionResultAssertor AssertValueFromJsonResource(string resourceName, params string[] pathsToIgnore) @@ -129,7 +129,7 @@ public ActionResultAssertor AssertValueFromJsonResource(string resourceN /// /// The to infer the that contains the embedded resource. /// The value . - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected value as serialized JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected value as serialized JSON. /// The JSON paths to ignore from the comparison. /// The to support fluent-style method-chaining. public ActionResultAssertor AssertValueFromJsonResource(string resourceName, params string[] pathsToIgnore) @@ -362,7 +362,7 @@ private ActionResultAssertor AssertValue(TValue? expectedValue, object? /// /// Asserts that the has the specified Content matches the JSON value. /// - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected JSON. /// The JSON paths to ignore from the comparison. /// The to support fluent-style method-chaining. public ActionResultAssertor AssertJsonFromResource(string resourceName, params string[] pathsToIgnore) diff --git a/src/UnitTestEx/Assertors/Assertor.cs b/src/UnitTestEx/Assertors/Assertor.cs index df830ef..179c6bc 100644 --- a/src/UnitTestEx/Assertors/Assertor.cs +++ b/src/UnitTestEx/Assertors/Assertor.cs @@ -36,7 +36,7 @@ public static ApiError[] ConvertToApiErrors(IDictionary? error } /// - /// Trys to match the and contents. + /// Tries to match the and contents. /// /// The expected list. /// The actual list. @@ -56,13 +56,32 @@ public static bool TryAreErrorsMatched(IEnumerable? expected, IEnumera if (exp.Count > 0) { sb.AppendLine(" Expected messages not matched:"); - exp.ForEach(m => sb.AppendLine($" Error: {m.Message} {(m.Field != null ? $"[{m.Field}]" : null)}")); + exp.ForEach(m => sb.AppendLine($" > {(m.Field != null ? $"{m.Field}" : "null")}: {m.Message}")); } if (act.Count > 0) { + if (exp.Count > 0) + sb.AppendLine(); + sb.AppendLine(" Actual messages not matched:"); - act.ForEach(m => sb.AppendLine($" Error: {m.Message} {(m.Field != null ? $"[{m.Field}]" : null)}")); + act.ForEach(m => sb.AppendLine($" > {(m.Field != null ? $"{m.Field}" : "null")}: {m.Message}")); + } + + if (act.Count > 0) + { + sb.AppendLine().AppendLine(); + sb.AppendLine("All actual message tuples as follows:"); + + for (int i = 0; i < act.Count; i++) + { + var m = act[i]; + sb.Append($" ({(m.Field != null ? $"\"{m.Field}\"" : "null")}, \"{m.Message}\")"); + if (i < act.Count - 1) + sb.AppendLine(","); + else + sb.AppendLine(); + } } errorMessage = sb.Length > 0 ? $"Error messages mismatch:{System.Environment.NewLine}{sb}" : null; diff --git a/src/UnitTestEx/Assertors/HttpResponseMessageAssertorBase.cs b/src/UnitTestEx/Assertors/HttpResponseMessageAssertorBase.cs index 063d511..d2c233f 100644 --- a/src/UnitTestEx/Assertors/HttpResponseMessageAssertorBase.cs +++ b/src/UnitTestEx/Assertors/HttpResponseMessageAssertorBase.cs @@ -9,7 +9,7 @@ namespace UnitTestEx.Assertors { /// - /// Provdes the base test assert helper capabilities. + /// Provides the base test assert helper capabilities. /// /// /// Initializes a new instance of the class. diff --git a/src/UnitTestEx/Assertors/HttpResponseMessageAssertorBaseT.cs b/src/UnitTestEx/Assertors/HttpResponseMessageAssertorBaseT.cs index e4af85a..0f47bb6 100644 --- a/src/UnitTestEx/Assertors/HttpResponseMessageAssertorBaseT.cs +++ b/src/UnitTestEx/Assertors/HttpResponseMessageAssertorBaseT.cs @@ -14,7 +14,7 @@ namespace UnitTestEx.Assertors { /// - /// Provdes the base test assert helper capabilities. + /// Provides the base test assert helper capabilities. /// /// The owning . /// The . @@ -256,7 +256,7 @@ public TSelf AssertErrors(params ApiError[] errors) /// /// Asserts that the JSON content matches the JSON from the embedded resource. /// - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected JSON. /// The JSON paths to ignore from the comparison. /// The instance to support fluent-style method-chaining. public TSelf AssertJsonFromResource(string resourceName, params string[] pathsToIgnore) => AssertJson(Resource.GetJson(resourceName, Assembly.GetCallingAssembly()), pathsToIgnore); @@ -265,7 +265,7 @@ public TSelf AssertErrors(params ApiError[] errors) /// Asserts that the matches the JSON serialized value. /// /// The to infer the that contains the embedded resource. - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected value as serialized JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected value as serialized JSON. /// The JSON paths to ignore from the comparison. /// The instance to support fluent-style method-chaining. public TSelf AssertJsonFromResource(string resourceName, params string[] pathsToIgnore) => AssertJson(Resource.GetJson(resourceName, typeof(TAssembly).Assembly), pathsToIgnore); diff --git a/src/UnitTestEx/Assertors/ValueAssertor.cs b/src/UnitTestEx/Assertors/ValueAssertor.cs index 2183bd8..8db53f6 100644 --- a/src/UnitTestEx/Assertors/ValueAssertor.cs +++ b/src/UnitTestEx/Assertors/ValueAssertor.cs @@ -69,7 +69,7 @@ public ValueAssertor AssertJson(string json, params string[] pathsToIgno /// /// Asserts that the matches the JSON serialized value from the named embedded resource. /// - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected value as serialized JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected value as serialized JSON. /// The JSON paths to ignore from the comparison. /// The to support fluent-style method-chaining. public ValueAssertor AssertJsonFromResource(string resourceName, params string[] pathsToIgnore) @@ -79,7 +79,7 @@ public ValueAssertor AssertJsonFromResource(string resourceName, params /// Asserts that the matches the JSON serialized value from the named embedded resource. /// /// The to infer the that contains the embedded resource. - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected value as serialized JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected value as serialized JSON. /// The JSON paths to ignore from the comparison. /// The to support fluent-style method-chaining. public ValueAssertor AssertJsonFromResource(string resourceName, params string[] pathsToIgnore) diff --git a/src/UnitTestEx/Expectations/ExpectationsArranger.cs b/src/UnitTestEx/Expectations/ExpectationsArranger.cs index 2acc016..75efee0 100644 --- a/src/UnitTestEx/Expectations/ExpectationsArranger.cs +++ b/src/UnitTestEx/Expectations/ExpectationsArranger.cs @@ -107,7 +107,7 @@ public async Task AssertAsync(AssertArgs args) } /// - /// Resets any existing expectations back to their orginating assert state to allow for a re-execution. + /// Resets any existing expectations back to their originating assert state to allow for a re-execution. /// public void Reset() { diff --git a/src/UnitTestEx/Expectations/ExpectationsExtensions.cs b/src/UnitTestEx/Expectations/ExpectationsExtensions.cs index 51e9a0b..310a0e1 100644 --- a/src/UnitTestEx/Expectations/ExpectationsExtensions.cs +++ b/src/UnitTestEx/Expectations/ExpectationsExtensions.cs @@ -227,7 +227,7 @@ public static TSelf ExpectJson(this IValueExpectationsThe expectations . /// The value . /// The . - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected value as serialized JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected value as serialized JSON. /// The JSON paths to ignore. /// The instance to support fluent-style method-chaining. /// Uses to load the embedded resource within the . diff --git a/src/UnitTestEx/Expectations/IExpectations.cs b/src/UnitTestEx/Expectations/IExpectations.cs index 51f5b25..8a255a8 100644 --- a/src/UnitTestEx/Expectations/IExpectations.cs +++ b/src/UnitTestEx/Expectations/IExpectations.cs @@ -2,11 +2,16 @@ namespace UnitTestEx.Expectations { + /// + /// Enables the identification of a tester as having expectations capabilities. + /// + public interface IExpectations { } + /// /// Enables the Expectations fluent-style method-chaining capabilities. /// /// The representing itself. - public interface IExpectations where TSelf : IExpectations + public interface IExpectations : IExpectations where TSelf : IExpectations { /// /// Gets the . diff --git a/src/UnitTestEx/Hosting/EntryPoint.cs b/src/UnitTestEx/Hosting/EntryPoint.cs index 34ce68c..6833701 100644 --- a/src/UnitTestEx/Hosting/EntryPoint.cs +++ b/src/UnitTestEx/Hosting/EntryPoint.cs @@ -11,7 +11,7 @@ namespace UnitTestEx.Hosting /// /// Provides a generic to support dependency injection. /// - /// Uses reflection to map to the same named methods: , and . + /// Uses reflection to map to the same named methods: , and . These methods can be either instance or static methods. public class EntryPoint { private readonly object _instance; @@ -29,12 +29,12 @@ public class EntryPoint public EntryPoint(object instance) { _instance = instance ?? throw new ArgumentNullException(nameof(instance)); - _mi1 = instance.GetType().GetMethod(nameof(ConfigureAppConfiguration), BindingFlags.Instance | BindingFlags.Public, [typeof(HostBuilderContext), typeof(IConfigurationBuilder)]); - _mi2 = instance.GetType().GetMethod(nameof(ConfigureHostConfiguration), BindingFlags.Instance | BindingFlags.Public, [typeof(IConfigurationBuilder)]); - _mi3 = instance.GetType().GetMethod(nameof(ConfigureServices), BindingFlags.Instance | BindingFlags.Public, [typeof(IServiceCollection)]); - _mi3 = instance.GetType().GetMethod(nameof(ConfigureServices), BindingFlags.Instance | BindingFlags.Public, [typeof(IServiceCollection)]); + _mi1 = instance.GetType().GetMethod(nameof(ConfigureAppConfiguration), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(HostBuilderContext), typeof(IConfigurationBuilder)]); + _mi2 = instance.GetType().GetMethod(nameof(ConfigureHostConfiguration), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(IConfigurationBuilder)]); + _mi3 = instance.GetType().GetMethod(nameof(ConfigureServices), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(IServiceCollection)]); + _mi3 = instance.GetType().GetMethod(nameof(ConfigureServices), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(IServiceCollection)]); #if NET8_0_OR_GREATER - _mi4 = instance.GetType().GetMethod(nameof(ConfigureApplication), BindingFlags.Instance | BindingFlags.Public, [typeof(IHostApplicationBuilder)]); + _mi4 = instance.GetType().GetMethod(nameof(ConfigureApplication), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(IHostApplicationBuilder)]); #endif } diff --git a/src/UnitTestEx/Hosting/ScopedTypeTester.cs b/src/UnitTestEx/Hosting/ScopedTypeTester.cs index 0120417..7519419 100644 --- a/src/UnitTestEx/Hosting/ScopedTypeTester.cs +++ b/src/UnitTestEx/Hosting/ScopedTypeTester.cs @@ -45,77 +45,102 @@ public ScopedTypeTester(TesterBase owner, IServiceProvider serviceProvider, TSer /// Runs the synchronous method with no result. /// /// The function execution. + /// The optional . /// A . - public VoidAssertor Run(Action function) => RunAsync(x => { function(x); return Task.CompletedTask; }).GetAwaiter().GetResult(); + public VoidAssertor Run(Action function, TesterArgs? args = null) => RunAsync(x => { function(x); return Task.CompletedTask; }, args).GetAwaiter().GetResult(); /// /// Runs the synchronous method with a result. /// /// The result value . /// The function execution. + /// The optional . /// A . - public ValueAssertor Run(Func function) => RunAsync(x => Task.FromResult(function(x))).GetAwaiter().GetResult(); + public ValueAssertor Run(Func function, TesterArgs? args = null) => RunAsync(x => Task.FromResult(function(x)), args).GetAwaiter().GetResult(); /// /// Runs the asynchronous method with no result. /// /// The function execution. + /// The optional . /// A . #if NET9_0_OR_GREATER [OverloadResolutionPriority(1)] #endif - public VoidAssertor Run(Func function) => RunAsync(function).GetAwaiter().GetResult(); + public VoidAssertor Run(Func function, TesterArgs? args = null) => RunAsync(function, args).GetAwaiter().GetResult(); #if NET9_0_OR_GREATER /// /// Runs the asynchronous method with no result. /// /// The function execution. + /// The optional . /// A . [OverloadResolutionPriority(2)] - public VoidAssertor Run(Func function) => RunAsync(v => function(v).AsTask()).GetAwaiter().GetResult(); + public VoidAssertor Run(Func function, TesterArgs? args = null) => RunAsync(v => function(v).AsTask(), args).GetAwaiter().GetResult(); #endif /// /// Runs the asynchronous method with no result. /// /// The function execution. + /// The optional . /// A . #if NET9_0_OR_GREATER [OverloadResolutionPriority(1)] #endif - public async Task RunAsync(Func function) + public async Task RunAsync(Func function, TesterArgs? args = null) { - TestSetUp.LogAutoSetUpOutputs(Implementor); - - Exception? ex = null; - var sw = Stopwatch.StartNew(); - LogHeader(); + args ??= new TesterArgs(); + _ = Owner.SharedState.GetLoggerMessages(); try { - await (function ?? throw new ArgumentNullException(nameof(function)))(Service).ConfigureAwait(false); - } - catch (AggregateException aex) - { - ex = aex.InnerException ?? aex; - } - catch (Exception uex) - { - ex = uex; + TestSetUp.LogAutoSetUpOutputs(Implementor); + + if (!args.BypassRunActions) + Owner.ExecutePreRunActions(this); + + Exception? ex = null; + var sw = Stopwatch.StartNew(); + LogHeader(); + + try + { + await (function ?? throw new ArgumentNullException(nameof(function)))(Service).ConfigureAwait(false); + } + catch (AggregateException aex) + { + ex = aex.InnerException ?? aex; + } + catch (Exception uex) + { + ex = uex; + } + finally + { + sw.Stop(); + } + + await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); + var logs = Owner.SharedState.GetLoggerMessages(); + LogResult(ex, sw.Elapsed.TotalMilliseconds, logs); + + if (!args.BypassRunActions) + Owner.ExecutePostRunBeforeExpectationsActions(this); + + await ExpectationsArranger.AssertAsync(logs, ex).ConfigureAwait(false); + + if (!args.BypassRunActions) + Owner.ExecutePostRunAfterExpectationsActions(this); + + return new VoidAssertor(Owner, ex); } finally { - sw.Stop(); + if (!args.BypassRunActions) + Owner.ExecutePostRunActions(this); } - - await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); - var logs = Owner.SharedState.GetLoggerMessages(); - LogResult(ex, sw.Elapsed.TotalMilliseconds, logs); - - await ExpectationsArranger.AssertAsync(logs, ex).ConfigureAwait(false); - - return new VoidAssertor(Owner, ex); } #if NET9_0_OR_GREATER @@ -123,9 +148,10 @@ public async Task RunAsync(Func function) /// Runs the asynchronous method with no result. /// /// The function execution. + /// The optional . /// A . [OverloadResolutionPriority(2)] - public async Task RunAsync(Func function) => await RunAsync(v => function(v).AsTask()).ConfigureAwait(false); + public async Task RunAsync(Func function, TesterArgs? args = null) => await RunAsync(v => function(v).AsTask(), args).ConfigureAwait(false); #endif /// @@ -133,11 +159,12 @@ public async Task RunAsync(Func function) /// /// The result value . /// The function execution. + /// The optional . /// A . #if NET9_0_OR_GREATER [OverloadResolutionPriority(1)] #endif - public ValueAssertor Run(Func> function) => RunAsync(function).GetAwaiter().GetResult(); + public ValueAssertor Run(Func> function, TesterArgs? args = null) => RunAsync(function, args).GetAwaiter().GetResult(); #if NET9_0_OR_GREATER /// @@ -145,9 +172,10 @@ public async Task RunAsync(Func function) /// /// The result value . /// The function execution. + /// The optional . /// A . [OverloadResolutionPriority(2)] - public ValueAssertor Run(Func> function) => RunAsync(v => function(v).AsTask()).GetAwaiter().GetResult(); + public ValueAssertor Run(Func> function, TesterArgs? args = null) => RunAsync(v => function(v).AsTask(), args).GetAwaiter().GetResult(); #endif /// @@ -155,57 +183,78 @@ public async Task RunAsync(Func function) /// /// The result value . /// The function execution. + /// The optional . /// A . #if NET9_0_OR_GREATER [OverloadResolutionPriority(1)] #endif - public async Task> RunAsync(Func> function) + public async Task> RunAsync(Func> function, TesterArgs? args = null) { - TestSetUp.LogAutoSetUpOutputs(Implementor); - - TValue result = default!; - Exception? ex = null; - var sw = Stopwatch.StartNew(); - LogHeader(); + args ??= new TesterArgs(); + _ = Owner.SharedState.GetLoggerMessages(); try { - result = await (function ?? throw new ArgumentNullException(nameof(function)))(Service).ConfigureAwait(false); - } - catch (AggregateException aex) - { - ex = aex.InnerException ?? aex; - } - catch (Exception uex) - { - ex = uex; - } - finally - { - sw.Stop(); - } + TestSetUp.LogAutoSetUpOutputs(Implementor); - await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); - var logs = Owner.SharedState.GetLoggerMessages(); - LogResult(ex, sw.Elapsed.TotalMilliseconds, logs); + if (!args.BypassRunActions) + Owner.ExecutePreRunActions(this); - if (ex == null) - { - if (result is string str) - Implementor.WriteLine($"Result: {str}"); - else if (result is IFormattable ifm) - Implementor.WriteLine($"Result: {ifm.ToString(null, CultureInfo.CurrentCulture)}"); - else + TValue result = default!; + Exception? ex = null; + var sw = Stopwatch.StartNew(); + LogHeader(); + + try { - Implementor.WriteLine($"Result: {(result == null ? "" : result.GetType().Name)}"); - if (result != null) - Implementor.WriteLine(JsonSerializer.Serialize(result, JsonWriteFormat.Indented)); + result = await (function ?? throw new ArgumentNullException(nameof(function)))(Service).ConfigureAwait(false); + } + catch (AggregateException aex) + { + ex = aex.InnerException ?? aex; + } + catch (Exception uex) + { + ex = uex; + } + finally + { + sw.Stop(); + } + + await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); + var logs = Owner.SharedState.GetLoggerMessages(); + LogResult(ex, sw.Elapsed.TotalMilliseconds, logs); + + if (ex == null) + { + if (result is string str) + Implementor.WriteLine($"Result: {str}"); + else if (result is IFormattable ifm) + Implementor.WriteLine($"Result: {ifm.ToString(null, CultureInfo.CurrentCulture)}"); + else + { + Implementor.WriteLine($"Result: {(result == null ? "" : result.GetType().Name)}"); + if (result != null) + Implementor.WriteLine(JsonSerializer.Serialize(result, JsonWriteFormat.Indented)); + } } - } - await ExpectationsArranger.AssertValueAsync(logs, result, ex).ConfigureAwait(false); + if (!args.BypassRunActions) + Owner.ExecutePostRunBeforeExpectationsActions(this); - return new ValueAssertor(Owner, result, ex); + await ExpectationsArranger.AssertValueAsync(logs, result, ex).ConfigureAwait(false); + + if (!args.BypassRunActions) + Owner.ExecutePostRunAfterExpectationsActions(this); + + return new ValueAssertor(Owner, result, ex); + } + finally + { + if (!args.BypassRunActions) + Owner.ExecutePostRunActions(this); + } } #if NET9_0_OR_GREATER @@ -214,9 +263,10 @@ public async Task> RunAsync(Func /// The result value . /// The function execution. + /// The optional . /// A . [OverloadResolutionPriority(2)] - public async Task> RunAsync(Func> function) => await RunAsync(v => function(v).AsTask()).ConfigureAwait(false); + public async Task> RunAsync(Func> function, TesterArgs? args = null) => await RunAsync(v => function(v).AsTask(), args).ConfigureAwait(false); #endif /// diff --git a/src/UnitTestEx/Mocking/MockHttpClient.cs b/src/UnitTestEx/Mocking/MockHttpClient.cs index 792a238..8b4d9ff 100644 --- a/src/UnitTestEx/Mocking/MockHttpClient.cs +++ b/src/UnitTestEx/Mocking/MockHttpClient.cs @@ -331,14 +331,14 @@ private class MockHttpMessageHandlerBuilder(string? name, IServiceProvider servi /// Adds mocked request(s) from the embedded resource formatted as either YAML or JSON. /// /// The used to infer that contains the embedded resource. - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// public MockHttpClient WithRequestsFromResource(string resourceName) => WithRequestsFromResource(resourceName, typeof(TAssembly).Assembly); /// /// Adds mocked request(s) from the embedded resource formatted as either YAML or JSON. /// - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The that contains the embedded resource; defaults to . /// The to support fluent-style method-chaining. public MockHttpClient WithRequestsFromResource(string resourceName, Assembly? assembly = null) diff --git a/src/UnitTestEx/Mocking/MockHttpClientRequest.cs b/src/UnitTestEx/Mocking/MockHttpClientRequest.cs index 4c03899..7540c9d 100644 --- a/src/UnitTestEx/Mocking/MockHttpClientRequest.cs +++ b/src/UnitTestEx/Mocking/MockHttpClientRequest.cs @@ -239,12 +239,29 @@ public override string ToString() return _content.ToString(); } + /// + /// Reset to initial state; ensure consistency to allow reuse of the same instance. + /// + private void Reset() + { + IsMockComplete = false; + _anyContent = false; + _content = null; + _mediaType = null; + _pathsToIgnore = []; + _traceRequestComparisons = false; + + Rule.Reset(); + Rule.Response = new MockHttpClientResponse(this, Rule); + } + /// /// Enables any request with a body (functionally equivalent to ). /// /// The resulting to accordingly. public MockHttpClientRequestBody WithAnyBody() { + Reset(); _anyContent = true; _mediaType = MediaTypeNames.Text.Plain; return new MockHttpClientRequestBody(Rule); @@ -257,6 +274,7 @@ public MockHttpClientRequestBody WithAnyBody() /// The resulting to accordingly. public MockHttpClientRequestBody WithBody(string text) { + Reset(); _content = text; _mediaType = MediaTypeNames.Text.Plain; return new MockHttpClientRequestBody(Rule); @@ -270,6 +288,7 @@ public MockHttpClientRequestBody WithBody(string text) /// The resulting to accordingly. public MockHttpClientRequestBody WithBody(string body, string mediaType) { + Reset(); _content = body; _mediaType = mediaType; return new MockHttpClientRequestBody(Rule); @@ -284,6 +303,7 @@ public MockHttpClientRequestBody WithBody(string body, string mediaType) /// The resulting to accordingly. public MockHttpClientRequestBody WithJsonBody(T value, params string[] pathsToIgnore) { + Reset(); _content = value; _mediaType = MediaTypeNames.Application.Json; _pathsToIgnore = pathsToIgnore; @@ -302,6 +322,8 @@ public MockHttpClientRequestBody WithJsonBody([StringSyntax(StringSyntaxAttribut public MockHttpClientRequestBody WithJsonBody(string json, params string[] pathsToIgnore) #endif { + Reset(); + try { _ = JsonSerializer.Deserialize(json); @@ -320,7 +342,7 @@ public MockHttpClientRequestBody WithJsonBody(string json, params string[] paths /// Provides the expected request body using the JSON formatted embedded resource as the content (). /// /// The used to infer that contains the embedded resource. - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The JSON paths to ignore from the comparison. /// The resulting to accordingly. public MockHttpClientRequestBody WithJsonResourceBody(string resourceName, params string[] pathsToIgnore) => WithJsonResourceBody(resourceName, typeof(TAssembly).Assembly, pathsToIgnore); @@ -328,12 +350,13 @@ public MockHttpClientRequestBody WithJsonBody(string json, params string[] paths /// /// Provides the expected request body using the JSON formatted embedded resource as the content (). /// - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The that contains the embedded resource; defaults to . /// The JSON paths to ignore from the comparison. /// The resulting to accordingly. public MockHttpClientRequestBody WithJsonResourceBody(string resourceName, Assembly? assembly = null, params string[] pathsToIgnore) { + Reset(); _content = Resource.GetJson(resourceName, assembly ?? Assembly.GetCallingAssembly()); _pathsToIgnore = pathsToIgnore; _mediaType = MediaTypeNames.Application.Json; diff --git a/src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs b/src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs index bd68b08..805b189 100644 --- a/src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs +++ b/src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs @@ -34,5 +34,17 @@ internal class MockHttpClientRequestRule /// Gets or sets the number of . /// internal Times? Times { get; set; } + + /// + /// Resets the rule to the default values. + /// + /// Does not reset the or . + internal void Reset() + { + Body = null; + Response = null; + Responses = null; + ResponsesIndex = 0; + } } } \ No newline at end of file diff --git a/src/UnitTestEx/Mocking/MockHttpClientResponse.cs b/src/UnitTestEx/Mocking/MockHttpClientResponse.cs index f337edc..66052c2 100644 --- a/src/UnitTestEx/Mocking/MockHttpClientResponse.cs +++ b/src/UnitTestEx/Mocking/MockHttpClientResponse.cs @@ -104,16 +104,16 @@ public MockHttpClientResponse Delay(TimeSpan from, TimeSpan to) /// /// Sets the simulated delay (sleep) for the response. /// - /// The delay (sleep) milliseconds. + /// The delay (sleep) milliseconds. /// The to support fluent-style method-chaining. /// Each time a Delay is invoked it will override the previously set value. - public MockHttpClientResponse Delay(int millseconds) => Delay(TimeSpan.FromMilliseconds(millseconds)); + public MockHttpClientResponse Delay(int milliseconds) => Delay(TimeSpan.FromMilliseconds(milliseconds)); /// /// Sets the simulated delay (sleep) as a random between the and values for the response. /// /// The from milliseconds. - /// The to millseconds. + /// The to milliseconds. /// The to support fluent-style method-chaining. /// Each time a Delay is invoked it will override the previously set value. public MockHttpClientResponse Delay(int from, int to) => Delay(TimeSpan.FromMilliseconds(from), TimeSpan.FromMilliseconds(to)); @@ -221,7 +221,7 @@ public void WithJson(string json, HttpStatusCode? statusCode = null, string? med /// Provides the mocked response using the JSON formatted embedded resource string as the content. /// /// The used to infer that contains the embedded resource. - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The optional (defaults to ). /// The optional media type (defaults to ). /// The optional action to enable additional configuration of the . @@ -231,7 +231,7 @@ public void WithJsonResource(string resourceName, HttpStatusCode? sta /// /// Provides the mocked response using the JSON formatted embedded resource string as the content. /// - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The optional (defaults to ). /// The optional media type (defaults to ). /// The optional action to enable additional configuration of the . diff --git a/src/UnitTestEx/ObjectComparer.cs b/src/UnitTestEx/ObjectComparer.cs index 105da5c..22b2a52 100644 --- a/src/UnitTestEx/ObjectComparer.cs +++ b/src/UnitTestEx/ObjectComparer.cs @@ -138,7 +138,7 @@ public static void AssertJson(JsonElementComparerOptions? options, string? expec /// /// Compares two JSON strings to each other where the expected JSON is from an embedded resource. /// - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected JSON. /// The actual JSON. /// The JSON paths to ignore from the comparison. public static void AssertJsonFromResource(string resourceName, string? actual, params string[] pathsToIgnore) @@ -148,7 +148,7 @@ public static void AssertJsonFromResource(string resourceName, string? actual, p /// Compares two JSON strings to each other where the expected JSON is from an embedded resource. /// /// The to infer the that contains the embedded resource. - /// The embedded resource name (matches to the end of the fully qualifed resource name) that contains the expected JSON. + /// The embedded resource name (matches to the end of the fully qualified resource name) that contains the expected JSON. /// The actual JSON. /// The JSON paths to ignore from the comparison. public static void AssertJsonFromResource(string resourceName, string? actual, params string[] pathsToIgnore) diff --git a/src/UnitTestEx/Resource.cs b/src/UnitTestEx/Resource.cs index 39f715f..715eb3f 100644 --- a/src/UnitTestEx/Resource.cs +++ b/src/UnitTestEx/Resource.cs @@ -19,7 +19,7 @@ public static class Resource /// /// Gets the named embedded resource . /// - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The that contains the embedded resource; defaults to . /// The ; otherwise, an will be thrown. public static StreamReader GetStream(string resourceName, Assembly? assembly = null) @@ -38,7 +38,7 @@ public static StreamReader GetStream(string resourceName, Assembly? assembly = n /// /// Gets the named embedded resource as a . /// - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The that contains the embedded resource; defaults to . /// The JSON . public static string GetString(string resourceName, Assembly? assembly = null) @@ -51,7 +51,7 @@ public static string GetString(string resourceName, Assembly? assembly = null) /// Gets the value by deserializing the JSON within the named embedded resource to the specified of . /// /// The value . - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The that contains the embedded resource; defaults to . /// The ; defaults to . /// The deserialized value. @@ -64,7 +64,7 @@ public static T GetJsonValue(string resourceName, Assembly? assembly = null, /// /// Gets the named embedded resource as a validated JSON . /// - /// The embedded resource name (matches to the end of the fully qualifed resource name). + /// The embedded resource name (matches to the end of the fully qualified resource name). /// The that contains the embedded resource; defaults to . /// The JSON . public static string GetJson(string resourceName, Assembly? assembly = null) diff --git a/src/UnitTestEx/TestSetUp.cs b/src/UnitTestEx/TestSetUp.cs index 552a808..2e48e61 100644 --- a/src/UnitTestEx/TestSetUp.cs +++ b/src/UnitTestEx/TestSetUp.cs @@ -242,7 +242,7 @@ public void RegisterAutoSetUp(Func - /// Executes the registered set up synchronsously. + /// Executes the registered set up synchronously. /// /// The optional data. /// true indicates that the set up was successful; otherwise, false. @@ -250,7 +250,7 @@ public void RegisterAutoSetUp(Func SetUpAsync(data).GetAwaiter().GetResult(); /// - /// Executes the registered set up asynchronsously. + /// Executes the registered set up asynchronously. /// /// The optional data. /// The . diff --git a/src/UnitTestEx/TesterArgs.cs b/src/UnitTestEx/TesterArgs.cs new file mode 100644 index 0000000..211d7e1 --- /dev/null +++ b/src/UnitTestEx/TesterArgs.cs @@ -0,0 +1,16 @@ +using UnitTestEx.Abstractions; + +namespace UnitTestEx +{ + /// + /// Provides per tester arguments that can be used to control the tester behavior. + /// + public class TesterArgs + { + /// + /// Indicates whether to bypass the execution of the configured run actions. + /// + /// The run actions are: , and . + public bool BypassRunActions { get; set; } + } +} \ No newline at end of file diff --git a/tests/UnitTestEx.NUnit.Test/MockHttpClientTest.cs b/tests/UnitTestEx.NUnit.Test/MockHttpClientTest.cs index 9526ca3..2eebc52 100644 --- a/tests/UnitTestEx.NUnit.Test/MockHttpClientTest.cs +++ b/tests/UnitTestEx.NUnit.Test/MockHttpClientTest.cs @@ -164,8 +164,9 @@ public async Task UriAndBody_WithJsonResponse2() public async Task UriAndBody_WithJsonResponse3() { var mcf = MockHttpClientFactory.Create(); - mcf.CreateClient("XXX", new Uri("https://d365test")) - .Request(HttpMethod.Post, "products/xyz").WithJsonBody(new Person { FirstName = "Bob", LastName = "Jane" }) + var req = mcf.CreateClient("XXX", new Uri("https://d365test")).Request(HttpMethod.Post, "products/xyz"); + + req.WithJsonBody(new Person { FirstName = "Bob", LastName = "Jane" }) .Respond.WithJsonResource("MockHttpClientTest-UriAndBody_WithJsonResponse3.json", HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); @@ -175,6 +176,41 @@ public async Task UriAndBody_WithJsonResponse3() Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); Assert.That(await res.Content.ReadAsStringAsync().ConfigureAwait(false), Is.EqualTo("{\"first\":\"Bob\",\"last\":\"Jane\"}")); }); + + // Change up the request body and now should fail. + req.WithJsonBody(new Person { FirstName = "Bob", LastName = "Janet" }) + .Respond.WithJsonResource("MockHttpClientTest-UriAndBody_WithJsonResponse3.json", HttpStatusCode.Accepted); + + try + { + res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + Assert.Fail(); + } + catch (MockHttpClientException mhcex) + { + Assert.That(mhcex.Message, Is.EqualTo("No corresponding MockHttpClient response found for HTTP request POST https://d365test/products/xyz {\"firstName\":\"Bob\",\"lastName\":\"Jane\"} (application/json)")); + } + } + + [Test] + public async Task Mock_Request_Not_Found() + { + var mcf = MockHttpClientFactory.Create(); + var req = mcf.CreateClient("XXX", new Uri("https://d365test")).Request(HttpMethod.Post, "products/xyz"); + + req.WithJsonBody(new Person { FirstName = "Bob", LastName = "Jane" }) + .Respond.WithJsonResource("MockHttpClientTest-UriAndBody_WithJsonResponse3.json", HttpStatusCode.Accepted); + + var hc = mcf.GetHttpClient("XXX"); + try + { + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bobby" }).ConfigureAwait(false); + Assert.Fail(); + } + catch (MockHttpClientException mhcex) + { + Assert.That(mhcex.Message, Is.EqualTo("No corresponding MockHttpClient response found for HTTP request POST https://d365test/products/xyz {\"firstName\":\"Bobby\",\"lastName\":\"Jane\"} (application/json)")); + } } [Test] diff --git a/tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs b/tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs index 04d18ba..7e9900f 100644 --- a/tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs +++ b/tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs @@ -110,7 +110,7 @@ public class EntryPoint //public void ConfigureServices(IServiceCollection services) { } - public void ConfigureApplication(IHostApplicationBuilder builder) + public static void ConfigureApplication(IHostApplicationBuilder builder) { builder.Services.AddSingleton(); } diff --git a/tests/UnitTestEx.NUnit.Test/Other/ObjectComparerTest.cs b/tests/UnitTestEx.NUnit.Test/Other/ObjectComparerTest.cs index 695b373..ddbd7f5 100644 --- a/tests/UnitTestEx.NUnit.Test/Other/ObjectComparerTest.cs +++ b/tests/UnitTestEx.NUnit.Test/Other/ObjectComparerTest.cs @@ -21,5 +21,14 @@ public void PathsToIgnore_PropertyName() var p2 = new Person { FirstName = "Wendy", LastName = "YYY" }; ObjectComparer.Assert(p1, p2, "LastName"); } + + [Test] + public void PathsToIgnore_Json() + { + // Starting array and object are ignored, so the path to ignore is just the property name. + var j1 = @"[ { ""FirstName"": ""Wendy"", ""LastName"": ""XXX"" } ]"; + var j2 = @"[ { ""FirstName"": ""Wendy"", ""LastName"": ""YYY"" } ]"; + ObjectComparer.AssertJson(j1, j2, "LastName"); + } } } \ No newline at end of file diff --git a/tests/UnitTestEx.NUnit.Test/PersonControllerTest.cs b/tests/UnitTestEx.NUnit.Test/PersonControllerTest.cs index a1436c2..61f4784 100644 --- a/tests/UnitTestEx.NUnit.Test/PersonControllerTest.cs +++ b/tests/UnitTestEx.NUnit.Test/PersonControllerTest.cs @@ -149,8 +149,8 @@ public void Update_Test3() .Run(c => c.Update(1, new Person { FirstName = null, LastName = null })) .AssertBadRequest() .AssertErrors( - new ApiError("firstName", "First name is required."), - new ApiError("lastName", "Last name is required.")); + ("firstName", "First name is required."), + "Last name is required."); } [Test] @@ -179,7 +179,7 @@ public void Update_Test5_ExpectationFailure() .Run(c => c.Update(1, new Person { FirstName = null, LastName = null })); }); - Assert.That(ex.Message, Does.Contain("Error: First name is requiredx.")); + Assert.That(ex.Message, Does.Contain("null: First name is requiredx.")); } [Test] From 4d6a12e03c5331b4a6a734390877de3e6d1e203c Mon Sep 17 00:00:00 2001 From: "Eric Sibly [chullybun]" Date: Sun, 29 Mar 2026 11:46:51 -0700 Subject: [PATCH 2/5] Update src/UnitTestEx/Hosting/EntryPoint.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Eric Sibly [chullybun] --- src/UnitTestEx/Hosting/EntryPoint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UnitTestEx/Hosting/EntryPoint.cs b/src/UnitTestEx/Hosting/EntryPoint.cs index 6833701..1e128f8 100644 --- a/src/UnitTestEx/Hosting/EntryPoint.cs +++ b/src/UnitTestEx/Hosting/EntryPoint.cs @@ -32,7 +32,6 @@ public EntryPoint(object instance) _mi1 = instance.GetType().GetMethod(nameof(ConfigureAppConfiguration), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(HostBuilderContext), typeof(IConfigurationBuilder)]); _mi2 = instance.GetType().GetMethod(nameof(ConfigureHostConfiguration), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(IConfigurationBuilder)]); _mi3 = instance.GetType().GetMethod(nameof(ConfigureServices), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(IServiceCollection)]); - _mi3 = instance.GetType().GetMethod(nameof(ConfigureServices), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(IServiceCollection)]); #if NET8_0_OR_GREATER _mi4 = instance.GetType().GetMethod(nameof(ConfigureApplication), BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public, [typeof(IHostApplicationBuilder)]); #endif From 061a4f05964451afd0b8a65d60df188209b27d66 Mon Sep 17 00:00:00 2001 From: "Eric Sibly [chullybun]" Date: Sun, 29 Mar 2026 11:47:14 -0700 Subject: [PATCH 3/5] Update src/UnitTestEx/Abstractions/TesterBase.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Eric Sibly [chullybun] --- src/UnitTestEx/Abstractions/TesterBase.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/UnitTestEx/Abstractions/TesterBase.cs b/src/UnitTestEx/Abstractions/TesterBase.cs index bec904c..c90e0f0 100644 --- a/src/UnitTestEx/Abstractions/TesterBase.cs +++ b/src/UnitTestEx/Abstractions/TesterBase.cs @@ -15,7 +15,6 @@ using System.Reflection; using System.Text; using System.Threading; -using UnitTestEx.AspNetCore; using UnitTestEx.Expectations; using UnitTestEx.Json; using UnitTestEx.Logging; From ea43b2cd49af4442863b06d96e66ecb13211379c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:50:06 +0000 Subject: [PATCH 4/5] Fix: Replace ExecutePreRunActions with ExecutePostRunActions in HttpTesterBase finally block Agent-Logs-Url: https://github.com/Avanade/UnitTestEx/sessions/aaa8d366-8973-4d7e-9cf4-e99050cedaf0 Co-authored-by: chullybun <12836934+chullybun@users.noreply.github.com> --- src/UnitTestEx/AspNetCore/HttpTesterBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnitTestEx/AspNetCore/HttpTesterBase.cs b/src/UnitTestEx/AspNetCore/HttpTesterBase.cs index 3c119e9..85df143 100644 --- a/src/UnitTestEx/AspNetCore/HttpTesterBase.cs +++ b/src/UnitTestEx/AspNetCore/HttpTesterBase.cs @@ -175,7 +175,7 @@ protected async Task SendAsync(HttpMethod httpMetho } finally { - Owner.ExecutePreRunActions(this); + Owner.ExecutePostRunActions(this); Owner.SharedState.RemoveRequestStateData(RequestId); } } From 8b4d7f164656a733360282f457de9986f3fc053f Mon Sep 17 00:00:00 2001 From: Eric Sibly Date: Sun, 29 Mar 2026 11:52:19 -0700 Subject: [PATCH 5/5] Correct comment. --- src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs b/src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs index 805b189..54f065d 100644 --- a/src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs +++ b/src/UnitTestEx/Mocking/MockHttpClientRequestRule.cs @@ -38,7 +38,7 @@ internal class MockHttpClientRequestRule /// /// Resets the rule to the default values. /// - /// Does not reset the or . + /// Does not reset ; the should be immediately updated post invocation to avoid runtime issues. internal void Reset() { Body = null;