From 53783dfa0372f3a3ef857de66639b5fdf770e0af Mon Sep 17 00:00:00 2001 From: Eric Sibly Date: Wed, 10 Jan 2024 17:23:32 -0800 Subject: [PATCH 1/3] Enable isolated functions. --- CHANGELOG.md | 8 + Common.targets | 2 +- src/UnitTestEx.MSTest/FunctionTester.cs | 3 +- .../Internal/FunctionTesterT.cs | 3 +- .../UnitTestEx.MSTest.csproj | 2 +- src/UnitTestEx.NUnit/FunctionTester.cs | 5 +- .../Internal/FunctionTesterT.cs | 5 +- .../Internal/NUnitTestImplementor.cs | 8 +- src/UnitTestEx.NUnit/UnitTestEx.NUnit.csproj | 2 +- .../Internal/FunctionTesterT.cs | 5 +- src/UnitTestEx.Xunit/UnitTestBase.cs | 7 +- src/UnitTestEx.Xunit/UnitTestEx.Xunit.csproj | 2 +- src/UnitTestEx/Abstractions/TesterBaseT.cs | 34 +++- src/UnitTestEx/Functions/HttpTriggerTester.cs | 22 +-- .../Functions/ServiceBusTriggerTester.cs | 77 ++++++-- ...ebJobsServiceBusMessageActionsAssertor.cs} | 30 +-- ...erviceBusSessionMessageActionsAssertor.cs} | 30 +-- .../WorkerServiceBusMessageActionsAssertor.cs | 171 ++++++++++++++++++ src/UnitTestEx/Hosting/HostTesterBase.cs | 28 ++- src/UnitTestEx/UnitTestEx.csproj | 19 +- .../UnitTestEx.Function.csproj | 2 +- .../UnitTestEx.IsolatedFunction.csproj | 14 +- .../UnitTestEx.MSTest.Test.csproj | 6 +- .../MockHttpClientTest.cs | 90 ++++----- .../Other/ExpectationsTest.cs | 20 +- .../Other/OneOffTestSetUpTest.cs | 8 +- .../PersonControllerTest.cs | 2 +- .../ProductControllerTest.cs | 8 +- .../ServiceBusFunctionTest.cs | 8 +- .../UnitTestEx.NUnit.Test.csproj | 8 +- .../MockHttpClientTest.cs | 72 ++++---- .../ProductControllerTest.cs | 7 +- .../ServiceBusFunctionTest.cs | 7 +- .../UnitTestEx.Xunit.Test.csproj | 6 +- 34 files changed, 489 insertions(+), 232 deletions(-) rename src/UnitTestEx/Functions/{ServiceBusMessageActionsAssertor.cs => WebJobsServiceBusMessageActionsAssertor.cs} (83%) rename src/UnitTestEx/Functions/{ServiceBusSessionMessageActionsAssertor.cs => WebJobsServiceBusSessionMessageActionsAssertor.cs} (84%) create mode 100644 src/UnitTestEx/Functions/WorkerServiceBusMessageActionsAssertor.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dee917..f1625a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ Represents the **NuGet** versions. +## v4.1.0 +- *Enhancement:* Removed the `FunctionsStartup` constraint for `TEntryPoint` to enable more generic usage. +- *Enhancement:* Enable `Microsoft.Azure.Functions.Worker.HttpTriggerAttribute` (new [_isolated_](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide) function support), in addition to existing `Microsoft.Azure.WebJobs.HttpTriggerAttribute` (existing [_in-process_](https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) function support), within `HttpTriggerTester`. +- *Enhancement:* Enable `Microsoft.Azure.Functions.Worker.ServiceBusTriggerAttribute` (new [_isolated_](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide) function support), in addition to existing `Microsoft.Azure.WebJobs.ServiceBusTriggerAttribute` (existing [_in-process_](https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) function support), within `ServiceBusTriggerTester`. + - Additionally, `CreateServiceBusMessageActions` is being renamed to `CreateWebJobsServiceBusMessageActions`; a new `CreateWorkerServiceBusMessageActions` has been introduced to support _isolated_ `Microsoft.Azure.Functions.Worker.ServiceBusTriggerAttribute` testing. +- *Enhancement*: Upgraded `NUnit` dependency to `4.0.1`; all unit tests now leverage the `NUnit` constraint model testing approach. + - **Note**: Also, as a result it is recommended prior to upgrading to `v4.1.0`, where using `NUnit`, that all existing unit tests are updated to use the new constraint model testing approach; see [migration guide](https://docs.nunit.org/articles/nunit/release-notes/Nunit4.0-MigrationGuide.html) for details. + ## v4.0.1 - *Fixed:* The `FunctionTesterBase` was updated to correctly load the configuration in the order similar to that performed by the Azure Functions runtime fabric. - *Fixed:* Removed all dependencies to `Newtonsoft.Json`; a developer will need to explicitly add this dependency and `IJsonSerializer` implementation where applicable. diff --git a/Common.targets b/Common.targets index 94a5abb..b16526d 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 4.0.1 + 4.0.2 preview Avanade Avanade diff --git a/src/UnitTestEx.MSTest/FunctionTester.cs b/src/UnitTestEx.MSTest/FunctionTester.cs index 3157b7c..a5afb84 100644 --- a/src/UnitTestEx.MSTest/FunctionTester.cs +++ b/src/UnitTestEx.MSTest/FunctionTester.cs @@ -1,6 +1,5 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx -using Microsoft.Azure.Functions.Extensions.DependencyInjection; using System; using System.Collections.Generic; using UnitTestEx.MSTest.Internal; @@ -21,7 +20,7 @@ public static class FunctionTester /// Additional configuration values to add/override. /// The . public static FunctionTester Create(bool? includeUnitTestConfiguration = null, bool? includeUserSecrets = null, IEnumerable>? additionalConfiguration = null) - where TEntryPoint : FunctionsStartup, new() + where TEntryPoint : class, new() => new(includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration); } } \ No newline at end of file diff --git a/src/UnitTestEx.MSTest/Internal/FunctionTesterT.cs b/src/UnitTestEx.MSTest/Internal/FunctionTesterT.cs index 1a7dd9e..b83adbb 100644 --- a/src/UnitTestEx.MSTest/Internal/FunctionTesterT.cs +++ b/src/UnitTestEx.MSTest/Internal/FunctionTesterT.cs @@ -1,6 +1,5 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx -using Microsoft.Azure.Functions.Extensions.DependencyInjection; using System.Collections.Generic; using UnitTestEx.Functions; @@ -14,5 +13,5 @@ namespace UnitTestEx.MSTest.Internal /// Indicates whether to include user secrets. /// Additional configuration values to add/override. public class FunctionTester(bool? includeUnitTestConfiguration, bool? includeUserSecrets, IEnumerable>? additionalConfiguration) - : FunctionTesterBase>(new MSTestImplementor(), includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration) where TEntryPoint : FunctionsStartup, new() { } + : FunctionTesterBase>(new MSTestImplementor(), includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration) where TEntryPoint : class, new() { } } \ No newline at end of file diff --git a/src/UnitTestEx.MSTest/UnitTestEx.MSTest.csproj b/src/UnitTestEx.MSTest/UnitTestEx.MSTest.csproj index 77b0009..07b8661 100644 --- a/src/UnitTestEx.MSTest/UnitTestEx.MSTest.csproj +++ b/src/UnitTestEx.MSTest/UnitTestEx.MSTest.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/UnitTestEx.NUnit/FunctionTester.cs b/src/UnitTestEx.NUnit/FunctionTester.cs index 46a84ca..2267f6d 100644 --- a/src/UnitTestEx.NUnit/FunctionTester.cs +++ b/src/UnitTestEx.NUnit/FunctionTester.cs @@ -1,6 +1,5 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx -using Microsoft.Azure.Functions.Extensions.DependencyInjection; using System; using System.Collections.Generic; using UnitTestEx.NUnit.Internal; @@ -15,13 +14,13 @@ public static class FunctionTester /// /// Creates a new instance of the class. /// - /// The API startup . + /// The Function startup . /// Indicates whether to include 'appsettings.unittest.json' configuration file. /// Indicates whether to include user secrets. /// Additional configuration values to add/override. /// The . public static FunctionTester Create(bool? includeUnitTestConfiguration = null, bool? includeUserSecrets = null, IEnumerable>? additionalConfiguration = null) - where TEntryPoint : FunctionsStartup, new() + where TEntryPoint : class, new() => new(includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration); } } \ No newline at end of file diff --git a/src/UnitTestEx.NUnit/Internal/FunctionTesterT.cs b/src/UnitTestEx.NUnit/Internal/FunctionTesterT.cs index 7538c11..bd87e88 100644 --- a/src/UnitTestEx.NUnit/Internal/FunctionTesterT.cs +++ b/src/UnitTestEx.NUnit/Internal/FunctionTesterT.cs @@ -1,6 +1,5 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx -using Microsoft.Azure.Functions.Extensions.DependencyInjection; using System.Collections.Generic; using UnitTestEx.Functions; @@ -9,10 +8,10 @@ namespace UnitTestEx.NUnit.Internal /// /// Provides the NUnit implementation. /// - /// + /// The Function startup . /// Indicates whether to include 'appsettings.unittest.json' configuration file. /// Indicates whether to include user secrets. /// Additional configuration values to add/override. public class FunctionTester(bool? includeUnitTestConfiguration, bool? includeUserSecrets, IEnumerable>? additionalConfiguration) - : FunctionTesterBase>(new NUnitTestImplementor(), includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration) where TEntryPoint : FunctionsStartup, new() { } + : FunctionTesterBase>(new NUnitTestImplementor(), includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration) where TEntryPoint : class, new() { } } \ No newline at end of file diff --git a/src/UnitTestEx.NUnit/Internal/NUnitTestImplementor.cs b/src/UnitTestEx.NUnit/Internal/NUnitTestImplementor.cs index 223bd01..28fbb07 100644 --- a/src/UnitTestEx.NUnit/Internal/NUnitTestImplementor.cs +++ b/src/UnitTestEx.NUnit/Internal/NUnitTestImplementor.cs @@ -9,6 +9,8 @@ namespace UnitTestEx.NUnit.Internal /// public sealed class NUnitTestImplementor : Abstractions.TestFrameworkImplementor { + private const string _notSpecifiedText = "Not specified."; + /// /// Creates a . /// @@ -16,13 +18,13 @@ public sealed class NUnitTestImplementor : Abstractions.TestFrameworkImplementor public static NUnitTestImplementor Create() => new(); /// - public override void AssertFail(string? message) => Assert.Fail(message); + public override void AssertFail(string? message) => Assert.Fail(message ?? _notSpecifiedText); /// - public override void AssertAreEqual(T? expected, T? actual, string? message = null) where T : default => Assert.AreEqual(expected, actual, message, [expected, actual]); + public override void AssertAreEqual(T? expected, T? actual, string? message = null) where T : default => Assert.That(actual, Is.EqualTo(expected), message); /// - public override void AssertInconclusive(string? message) => Assert.Inconclusive(message); + public override void AssertInconclusive(string? message) => Assert.Inconclusive(message ?? _notSpecifiedText); /// public override void WriteLine(string? message) => TestContext.Out.WriteLine(message); diff --git a/src/UnitTestEx.NUnit/UnitTestEx.NUnit.csproj b/src/UnitTestEx.NUnit/UnitTestEx.NUnit.csproj index 54baf2f..325c122 100644 --- a/src/UnitTestEx.NUnit/UnitTestEx.NUnit.csproj +++ b/src/UnitTestEx.NUnit/UnitTestEx.NUnit.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/UnitTestEx.Xunit/Internal/FunctionTesterT.cs b/src/UnitTestEx.Xunit/Internal/FunctionTesterT.cs index 4d4d458..951b2ee 100644 --- a/src/UnitTestEx.Xunit/Internal/FunctionTesterT.cs +++ b/src/UnitTestEx.Xunit/Internal/FunctionTesterT.cs @@ -1,6 +1,5 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx -using Microsoft.Azure.Functions.Extensions.DependencyInjection; using System.Collections.Generic; using UnitTestEx.Functions; using Xunit.Abstractions; @@ -10,11 +9,11 @@ namespace UnitTestEx.Xunit.Internal /// /// Provides the MSTest implementation. /// - /// + /// The Function startup . /// The . /// Indicates whether to include 'appsettings.unittest.json' configuration file. /// Indicates whether to include user secrets. /// Additional configuration values to add/override. public class FunctionTester(ITestOutputHelper output, bool? includeUnitTestConfiguration, bool? includeUserSecrets, IEnumerable>? additionalConfiguration) - : FunctionTesterBase>(new XunitTestImplementor(output), includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration) where TEntryPoint : FunctionsStartup, new() { } + : FunctionTesterBase>(new XunitTestImplementor(output), includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration) where TEntryPoint : class, new() { } } \ No newline at end of file diff --git a/src/UnitTestEx.Xunit/UnitTestBase.cs b/src/UnitTestEx.Xunit/UnitTestBase.cs index 1fd8ed6..a34d35a 100644 --- a/src/UnitTestEx.Xunit/UnitTestBase.cs +++ b/src/UnitTestEx.Xunit/UnitTestBase.cs @@ -1,6 +1,5 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx -using Microsoft.Azure.Functions.Extensions.DependencyInjection; using System; using System.Collections.Generic; using UnitTestEx.Hosting; @@ -35,15 +34,15 @@ public abstract class UnitTestBase(ITestOutputHelper output) protected ApiTester CreateApiTester() where TEntryPoint : class => new(Output); /// - /// Provides the Xunit API testing capability. + /// Provides the Xunit Functions testing capability. /// - /// The API startup . + /// The Function startup . /// Indicates whether to include 'appsettings.unittest.json' configuration file. /// Indicates whether to include user secrets. /// Additional configuration values to add/override. /// The . protected FunctionTester CreateFunctionTester(bool? includeUnitTestConfiguration = null, bool? includeUserSecrets = null, IEnumerable>? additionalConfiguration = null) - where TEntryPoint : FunctionsStartup, new() + where TEntryPoint : class, new() => new(Output, includeUnitTestConfiguration, includeUserSecrets, additionalConfiguration); /// diff --git a/src/UnitTestEx.Xunit/UnitTestEx.Xunit.csproj b/src/UnitTestEx.Xunit/UnitTestEx.Xunit.csproj index 3b1206f..6690ef6 100644 --- a/src/UnitTestEx.Xunit/UnitTestEx.Xunit.csproj +++ b/src/UnitTestEx.Xunit/UnitTestEx.Xunit.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/UnitTestEx/Abstractions/TesterBaseT.cs b/src/UnitTestEx/Abstractions/TesterBaseT.cs index a258bec..dcc33cc 100644 --- a/src/UnitTestEx/Abstractions/TesterBaseT.cs +++ b/src/UnitTestEx/Abstractions/TesterBaseT.cs @@ -359,18 +359,40 @@ public ServiceBusReceivedMessage CreateServiceBusMessage(AmqpAnnotatedMessage me } /// - /// Creates a as the instance to enable test mock and assert verification. + /// Creates a as the instance to enable test mock and assert verification. /// - /// The . - public ServiceBusMessageActionsAssertor CreateServiceBusMessageActions() => new(Implementor); + /// The . + [Obsolete("Please use either CreateWebJobsServiceBusMessageActions (existing behavior) or CreateWorkerServiceBusMessageActions as required. This method will be deprecated in a future release.", false)] + public WebJobsServiceBusMessageActionsAssertor CreateServiceBusMessageActions() => new(Implementor); /// - /// Creates a as the instance to enable test mock and assert verification. + /// Creates a as the instance to enable test mock and assert verification. + /// + /// The . + public WebJobsServiceBusMessageActionsAssertor CreateWebJobsServiceBusMessageActions() => new(Implementor); + + /// + /// Creates a as the instance to enable test mock and assert verification. /// /// The sessions locked until ; defaults to plus five minutes. /// The session state ; defaults to . - /// The . - public ServiceBusSessionMessageActionsAssertor CreateServiceBusSessionMessageActions(DateTimeOffset? sessionLockedUntil = default, BinaryData? sessionState = default) => new(Implementor, sessionLockedUntil, sessionState); + /// The . + [Obsolete("Please use CreateServiceBusSessionMessageActions (existing behavior). This method will be deprecated in a future release.", false)] + public WebJobsServiceBusSessionMessageActionsAssertor CreateServiceBusSessionMessageActions(DateTimeOffset? sessionLockedUntil = default, BinaryData? sessionState = default) => new(Implementor, sessionLockedUntil, sessionState); + + /// + /// Creates a as the instance to enable test mock and assert verification. + /// + /// The sessions locked until ; defaults to plus five minutes. + /// The session state ; defaults to . + /// The . + public WebJobsServiceBusSessionMessageActionsAssertor CreateWebJobsServiceBusSessionMessageActions(DateTimeOffset? sessionLockedUntil = default, BinaryData? sessionState = default) => new(Implementor, sessionLockedUntil, sessionState); + + /// + /// Creates a as the instance to enable test mock and assert verification. + /// + /// The . + public WorkerServiceBusMessageActionsAssertor CreateWorkerServiceBusMessageActions() => new(Implementor); #endregion } diff --git a/src/UnitTestEx/Functions/HttpTriggerTester.cs b/src/UnitTestEx/Functions/HttpTriggerTester.cs index 943ce95..317cf3e 100644 --- a/src/UnitTestEx/Functions/HttpTriggerTester.cs +++ b/src/UnitTestEx/Functions/HttpTriggerTester.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Azure.WebJobs; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -25,7 +24,7 @@ namespace UnitTestEx.Functions { /// - /// Provides Azure Function unit-testing capabilities. + /// Provides Azure Function or unit-testing capabilities. /// /// The Azure Function . public class HttpTriggerTester : HostTesterBase, IExpectations> where TFunction : class @@ -43,31 +42,32 @@ public class HttpTriggerTester : HostTesterBase, IExpectat public ExpectationsArranger> ExpectationsArranger { get; } /// - /// Runs the HTTP Triggered (see ) function using an within the . + /// Runs the HTTP Triggered (see or ) function using an within the . /// /// The function operation invocation expression. /// An . public ActionResultAssertor Run(Expression>> expression) => RunAsync(expression).GetAwaiter().GetResult(); /// - /// Runs the HTTP Triggered (see ) function using an within the . + /// Runs the HTTP Triggered (see or ) function using an within the . /// /// The function operation invocation expression. /// An . public async Task RunAsync(Expression>> expression) { - (IActionResult result, Exception? ex, double ms) = await RunAsync(expression, typeof(HttpTriggerAttribute), (p, a, v) => + (IActionResult result, Exception? ex, double ms) = await RunAsync(expression, [typeof(Microsoft.Azure.WebJobs.HttpTriggerAttribute), typeof(Microsoft.Azure.Functions.Worker.HttpTriggerAttribute)], (p, a, v) => { - if (a == null) - throw new InvalidOperationException($"The function method must have a parameter using the {nameof(HttpTriggerAttribute)}."); - var requestVal = v; var httpRequest = v as HttpRequest; LogRequest(httpRequest, requestVal); - var httpTriggerAttribute = (HttpTriggerAttribute)a; - if (httpRequest != null && !httpTriggerAttribute.Methods.Contains(httpRequest.Method, StringComparer.OrdinalIgnoreCase)) - throw new InvalidOperationException($"The function {nameof(HttpTriggerAttribute)} supports {nameof(HttpTriggerAttribute.Methods)} of {string.Join(" or ", httpTriggerAttribute.Methods.Select(x => $"'{x.ToUpperInvariant()}'"))}; however, invoked using '{httpRequest.Method.ToUpperInvariant()}' which is not valid."); + var httpTriggerAttribute = a as Microsoft.Azure.WebJobs.HttpTriggerAttribute; + if (httpRequest != null && httpTriggerAttribute is not null && !httpTriggerAttribute.Methods.Contains(httpRequest.Method, StringComparer.OrdinalIgnoreCase)) + throw new InvalidOperationException($"The function {nameof(Microsoft.Azure.WebJobs.HttpTriggerAttribute)} supports {nameof(Microsoft.Azure.WebJobs.HttpTriggerAttribute.Methods)} of {string.Join(" or ", httpTriggerAttribute.Methods.Select(x => $"'{x.ToUpperInvariant()}'"))}; however, invoked using '{httpRequest.Method.ToUpperInvariant()}' which is not valid."); + + var httpTriggerAttribute2 = a as Microsoft.Azure.Functions.Worker.HttpTriggerAttribute; + if (httpRequest != null && httpTriggerAttribute2 is not null && httpTriggerAttribute2.Methods is not null && !httpTriggerAttribute2.Methods!.Contains(httpRequest.Method, StringComparer.OrdinalIgnoreCase)) + throw new InvalidOperationException($"The function {nameof(Microsoft.Azure.Functions.Worker.HttpTriggerAttribute)} supports {nameof(Microsoft.Azure.Functions.Worker.HttpTriggerAttribute.Methods)} of {string.Join(" or ", httpTriggerAttribute2.Methods.Select(x => $"'{x.ToUpperInvariant()}'"))}; however, invoked using '{httpRequest.Method.ToUpperInvariant()}' which is not valid."); }).ConfigureAwait(false); await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); diff --git a/src/UnitTestEx/Functions/ServiceBusTriggerTester.cs b/src/UnitTestEx/Functions/ServiceBusTriggerTester.cs index 3bc0520..7f25cfc 100644 --- a/src/UnitTestEx/Functions/ServiceBusTriggerTester.cs +++ b/src/UnitTestEx/Functions/ServiceBusTriggerTester.cs @@ -1,7 +1,6 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx using Azure.Messaging.ServiceBus; -using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; @@ -20,7 +19,7 @@ namespace UnitTestEx.Functions { /// - /// Provides Azure Function unit-testing and integration emulation testing capabilities. + /// Provides Azure Function or unit-testing and integration emulation testing capabilities. /// /// The Azure Function . public class ServiceBusTriggerTester : HostTesterBase, IExpectations> where TFunction : class @@ -40,28 +39,29 @@ public class ServiceBusTriggerTester : HostTesterBase, IEx public ExpectationsArranger> ExpectationsArranger { get; } /// - /// Runs the Service Bus Triggered (see ) function expected as a parameter within the . + /// Runs the Service Bus Triggered ( or ) function expected as a parameter within the . /// /// The function operation invocation expression. - /// Indicates whether to validate the properties to ensure correct configuration. + /// Indicates whether to validate the or properties to ensure correct configuration. /// A . public VoidAssertor Run(Expression> expression, bool validateTriggerProperties = false) => RunAsync(expression, validateTriggerProperties).GetAwaiter().GetResult(); /// - /// Runs the Service Bus Triggered (see ) function expected as a parameter within the . + /// Runs the Service Bus Triggered ( or ) function expected as a parameter within the . /// /// The function operation invocation expression. - /// Indicates whether to validate the properties to ensure correct configuration. + /// Indicates whether to validate the or properties to ensure correct configuration. /// A . public async Task RunAsync(Expression> expression, bool validateTriggerProperties = false) { object? sbv = null; - ServiceBusMessageActionsAssertor? sba = null; - ServiceBusSessionMessageActionsAssertor? ssba = null; - (Exception? ex, double ms) = await RunAsync(expression, typeof(ServiceBusTriggerAttribute), (p, a, v) => + WebJobsServiceBusMessageActionsAssertor? sba = null; + WebJobsServiceBusSessionMessageActionsAssertor? ssba = null; + WorkerServiceBusMessageActionsAssertor? wsba = null; + (Exception? ex, double ms) = await RunAsync(expression, [typeof(Microsoft.Azure.WebJobs.ServiceBusTriggerAttribute), typeof(Microsoft.Azure.Functions.Worker.ServiceBusTriggerAttribute)], (p, a, v) => { - if (a == null) - throw new InvalidOperationException($"The function method must have a parameter using the {nameof(ServiceBusTriggerAttribute)}."); + if (validateTriggerProperties && a == null) + throw new InvalidOperationException($"The function method must have a parameter using the {nameof(Microsoft.Azure.WebJobs.ServiceBusTriggerAttribute)}."); Implementor.WriteLine(""); Implementor.WriteLine("FUNCTION SERVICE BUS TRIGGER TESTER..."); @@ -71,22 +71,30 @@ public async Task RunAsync(Expression> expre { if (pi is ServiceBusReceivedMessage sbrm) sbv = sbrm; - else if (pi is ServiceBusMessageActionsAssertor psba) + else if (pi is WebJobsServiceBusMessageActionsAssertor psba) sba = psba; - else if (pi is ServiceBusSessionMessageActionsAssertor pssba) + else if (pi is WebJobsServiceBusSessionMessageActionsAssertor pssba) ssba = pssba; + else if (pi is WorkerServiceBusMessageActionsAssertor pwsba) + wsba = pwsba; } - if (validateTriggerProperties) + if (validateTriggerProperties && a is not null) { var config = ServiceScope.ServiceProvider.GetRequiredService(); - VerifyServiceBusTriggerProperties(config, (ServiceBusTriggerAttribute)a); + var sbta = a as Microsoft.Azure.WebJobs.ServiceBusTriggerAttribute; + if (sbta is not null) + VerifyServiceBusTriggerProperties(config, sbta); + + var sbta2 = a as Microsoft.Azure.Functions.Worker.ServiceBusTriggerAttribute; + if (sbta2 is not null) + VerifyServiceBusTriggerProperties2(config, sbta2); } }).ConfigureAwait(false); await Task.Delay(TestSetUp.TaskDelayMilliseconds).ConfigureAwait(false); var logs = Owner.SharedState.GetLoggerMessages(); - LogOutput(ex, ms, sbv, sba, ssba, logs); + LogOutput(ex, ms, sbv, sba, ssba, wsba, logs); await ExpectationsArranger.AssertAsync(logs, ex).ConfigureAwait(false); @@ -96,7 +104,7 @@ public async Task RunAsync(Expression> expre /// /// Verifies the service bus trigger properties. /// - internal static (string ConnectionString, string? QueueName, string? TopicName, string? SubscriptionName) VerifyServiceBusTriggerProperties(IConfiguration config, ServiceBusTriggerAttribute sbta) + internal static (string ConnectionString, string? QueueName, string? TopicName, string? SubscriptionName) VerifyServiceBusTriggerProperties(IConfiguration config, Microsoft.Azure.WebJobs.ServiceBusTriggerAttribute sbta) { // Get the connection string. var csn = sbta.Connection ?? "ServiceBus"; @@ -125,6 +133,38 @@ internal static (string ConnectionString, string? QueueName, string? TopicName, return (cs, qn, tn, sn); } + /// + /// Verifies the service bus trigger properties. + /// + internal static (string ConnectionString, string? QueueName, string? TopicName, string? SubscriptionName) VerifyServiceBusTriggerProperties2(IConfiguration config, Microsoft.Azure.Functions.Worker.ServiceBusTriggerAttribute sbta) + { + // Get the connection string. + var csn = sbta.Connection ?? "ServiceBus"; + var cs = config.GetValue(csn); + if (cs == null) + { + csn = $"AzureWebJobs{csn}"; + cs = config.GetValue(csn); + } + + if (string.IsNullOrEmpty(cs)) + throw new InvalidOperationException("Service Bus Connection String configuration setting either does not exist or does not have a value."); + + // Get the queue name. + string? qn = null; + string? tn = null; + string? sn = null; + if (sbta.QueueName != null) + qn = GetValueFromConfig(config, "Queue Name", sbta.QueueName); + else + { + tn = GetValueFromConfig(config, "Topic Name", sbta.TopicName!); + sn = GetValueFromConfig(config, "Subscription Name", sbta.SubscriptionName!); + } + + return (cs, qn, tn, sn); + } + /// /// Gets the value from configuration. /// @@ -151,7 +191,7 @@ internal static string GetValueFromConfig(IConfiguration config, string type, st /// /// Log the output. /// - private void LogOutput(Exception? ex, double ms, object? value, ServiceBusMessageActionsAssertor? sba, ServiceBusSessionMessageActionsAssertor? ssba, IEnumerable? logs) + private void LogOutput(Exception? ex, double ms, object? value, WebJobsServiceBusMessageActionsAssertor? sba, WebJobsServiceBusSessionMessageActionsAssertor? ssba, WorkerServiceBusMessageActionsAssertor? wsba, IEnumerable? logs) { Implementor.WriteLine(""); Implementor.WriteLine("LOGGING >"); @@ -219,6 +259,7 @@ private void LogOutput(Exception? ex, double ms, object? value, ServiceBusMessag sba?.LogResult(); ssba?.LogResult(); + wsba?.LogResult(); Implementor.WriteLine(""); Implementor.WriteLine(new string('=', 80)); diff --git a/src/UnitTestEx/Functions/ServiceBusMessageActionsAssertor.cs b/src/UnitTestEx/Functions/WebJobsServiceBusMessageActionsAssertor.cs similarity index 83% rename from src/UnitTestEx/Functions/ServiceBusMessageActionsAssertor.cs rename to src/UnitTestEx/Functions/WebJobsServiceBusMessageActionsAssertor.cs index 277fe64..24af0f4 100644 --- a/src/UnitTestEx/Functions/ServiceBusMessageActionsAssertor.cs +++ b/src/UnitTestEx/Functions/WebJobsServiceBusMessageActionsAssertor.cs @@ -14,7 +14,7 @@ namespace UnitTestEx.Functions /// Provides a test mock and assert verification. /// /// The . - public class ServiceBusMessageActionsAssertor(TestFrameworkImplementor implementor) : ServiceBusMessageActions + public class WebJobsServiceBusMessageActionsAssertor(TestFrameworkImplementor implementor) : ServiceBusMessageActions { private readonly TestFrameworkImplementor _implementor = implementor ?? throw new ArgumentNullException(nameof(implementor)); @@ -115,7 +115,7 @@ private void VerifyAndSetActionStatus(ServiceBusMessageActionStatus status) } /// - /// Logs the result of the . + /// Logs the result of the . /// internal void LogResult() { @@ -145,7 +145,7 @@ internal void LogResult() /// /// Assert the status. /// - private ServiceBusMessageActionsAssertor AssertStatus(ServiceBusMessageActionStatus status) + private WebJobsServiceBusMessageActionsAssertor AssertStatus(ServiceBusMessageActionStatus status) { if (!Status.Equals(status)) _implementor.AssertAreEqual(status, Status, "ServiceBusMessageActions status is not equal."); @@ -156,28 +156,28 @@ private ServiceBusMessageActionsAssertor AssertStatus(ServiceBusMessageActionSta /// /// Asserts that the was invoked. /// - /// The to support fluent-style method-chaining. - public ServiceBusMessageActionsAssertor AssertAbandon() => AssertStatus(ServiceBusMessageActionStatus.Abandon); + /// The to support fluent-style method-chaining. + public WebJobsServiceBusMessageActionsAssertor AssertAbandon() => AssertStatus(ServiceBusMessageActionStatus.Abandon); /// /// Asserts that the was invoked. /// - /// The to support fluent-style method-chaining. - public ServiceBusMessageActionsAssertor AssertComplete() => AssertStatus(ServiceBusMessageActionStatus.Complete); + /// The to support fluent-style method-chaining. + public WebJobsServiceBusMessageActionsAssertor AssertComplete() => AssertStatus(ServiceBusMessageActionStatus.Complete); /// /// Asserts that the was invoked. /// - /// The to support fluent-style method-chaining. - public ServiceBusMessageActionsAssertor AssertDefer() => AssertStatus(ServiceBusMessageActionStatus.Defer); + /// The to support fluent-style method-chaining. + public WebJobsServiceBusMessageActionsAssertor AssertDefer() => AssertStatus(ServiceBusMessageActionStatus.Defer); /// /// Asserts that the one of the methods was invoked. /// /// Asserts that the resulting contains the specified content. /// Asserts that the resulting contains the specified content. - /// The to support fluent-style method-chaining. - public ServiceBusMessageActionsAssertor AssertDeadLetter(string? reasonContains = default, string? errorDescriptionContains = default) + /// The to support fluent-style method-chaining. + public WebJobsServiceBusMessageActionsAssertor AssertDeadLetter(string? reasonContains = default, string? errorDescriptionContains = default) { AssertStatus(ServiceBusMessageActionStatus.DeadLetter); if (!string.IsNullOrEmpty(reasonContains)) @@ -198,8 +198,8 @@ public ServiceBusMessageActionsAssertor AssertDeadLetter(string? reasonContains /// /// Asserts that the was invoked at least once or specified. /// - /// The to support fluent-style method-chaining. - public ServiceBusMessageActionsAssertor AssertRenew(int? count = default) + /// The to support fluent-style method-chaining. + public WebJobsServiceBusMessageActionsAssertor AssertRenew(int? count = default) { if (count.HasValue) _implementor.AssertAreEqual(count.Value, RenewCount, $"{nameof(RenewCount)} is not equal."); @@ -212,7 +212,7 @@ public ServiceBusMessageActionsAssertor AssertRenew(int? count = default) /// /// Asserts that no methods (with the exception of ) were invoked. /// - /// The to support fluent-style method-chaining. - public ServiceBusMessageActionsAssertor AssertNone() => AssertStatus(ServiceBusMessageActionStatus.None); + /// The to support fluent-style method-chaining. + public WebJobsServiceBusMessageActionsAssertor AssertNone() => AssertStatus(ServiceBusMessageActionStatus.None); } } \ No newline at end of file diff --git a/src/UnitTestEx/Functions/ServiceBusSessionMessageActionsAssertor.cs b/src/UnitTestEx/Functions/WebJobsServiceBusSessionMessageActionsAssertor.cs similarity index 84% rename from src/UnitTestEx/Functions/ServiceBusSessionMessageActionsAssertor.cs rename to src/UnitTestEx/Functions/WebJobsServiceBusSessionMessageActionsAssertor.cs index 4d0993f..c6ea07c 100644 --- a/src/UnitTestEx/Functions/ServiceBusSessionMessageActionsAssertor.cs +++ b/src/UnitTestEx/Functions/WebJobsServiceBusSessionMessageActionsAssertor.cs @@ -16,7 +16,7 @@ namespace UnitTestEx.Functions /// The . /// The sessions locked until ; defaults to plus five minutes. /// The session state ; defaults to . - public class ServiceBusSessionMessageActionsAssertor(TestFrameworkImplementor implementor, DateTimeOffset? sessionLockedUntil = default, BinaryData? sessionState = default) : ServiceBusSessionMessageActions + public class WebJobsServiceBusSessionMessageActionsAssertor(TestFrameworkImplementor implementor, DateTimeOffset? sessionLockedUntil = default, BinaryData? sessionState = default) : ServiceBusSessionMessageActions { private readonly TestFrameworkImplementor _implementor = implementor ?? throw new ArgumentNullException(nameof(implementor)); private DateTimeOffset _sessionLockedUntil = sessionLockedUntil ?? DateTimeOffset.UtcNow.AddMinutes(5); @@ -147,7 +147,7 @@ private void VerifyAndSetActionStatus(ServiceBusMessageActionStatus status) } /// - /// Logs the result of the . + /// Logs the result of the . /// internal void LogResult() { @@ -177,7 +177,7 @@ internal void LogResult() /// /// Assert the status. /// - private ServiceBusSessionMessageActionsAssertor AssertStatus(ServiceBusMessageActionStatus status) + private WebJobsServiceBusSessionMessageActionsAssertor AssertStatus(ServiceBusMessageActionStatus status) { if (!Status.Equals(status)) _implementor.AssertAreEqual(status, Status, "ServiceBusMessageActions status is not equal."); @@ -188,28 +188,28 @@ private ServiceBusSessionMessageActionsAssertor AssertStatus(ServiceBusMessageAc /// /// Asserts that the was invoked. /// - /// The to support fluent-style method-chaining. - public ServiceBusSessionMessageActionsAssertor AssertAbandon() => AssertStatus(ServiceBusMessageActionStatus.Abandon); + /// The to support fluent-style method-chaining. + public WebJobsServiceBusSessionMessageActionsAssertor AssertAbandon() => AssertStatus(ServiceBusMessageActionStatus.Abandon); /// /// Asserts that the was invoked. /// - /// The to support fluent-style method-chaining. - public ServiceBusSessionMessageActionsAssertor AssertComplete() => AssertStatus(ServiceBusMessageActionStatus.Complete); + /// The to support fluent-style method-chaining. + public WebJobsServiceBusSessionMessageActionsAssertor AssertComplete() => AssertStatus(ServiceBusMessageActionStatus.Complete); /// /// Asserts that the was invoked. /// - /// The to support fluent-style method-chaining. - public ServiceBusSessionMessageActionsAssertor AssertDefer() => AssertStatus(ServiceBusMessageActionStatus.Defer); + /// The to support fluent-style method-chaining. + public WebJobsServiceBusSessionMessageActionsAssertor AssertDefer() => AssertStatus(ServiceBusMessageActionStatus.Defer); /// /// Asserts that the one of the methods was invoked. /// /// Asserts that the resulting contains the specified content. /// Asserts that the resulting contains the specified content. - /// The to support fluent-style method-chaining. - public ServiceBusSessionMessageActionsAssertor AssertDeadLetter(string? reasonContains = default, string? errorDescriptionContains = default) + /// The to support fluent-style method-chaining. + public WebJobsServiceBusSessionMessageActionsAssertor AssertDeadLetter(string? reasonContains = default, string? errorDescriptionContains = default) { AssertStatus(ServiceBusMessageActionStatus.DeadLetter); if (!string.IsNullOrEmpty(reasonContains)) @@ -230,8 +230,8 @@ public ServiceBusSessionMessageActionsAssertor AssertDeadLetter(string? reasonCo /// /// Asserts that the was invoked at least once or specified. /// - /// The to support fluent-style method-chaining. - public ServiceBusSessionMessageActionsAssertor AssertRenew(int? count = default) + /// The to support fluent-style method-chaining. + public WebJobsServiceBusSessionMessageActionsAssertor AssertRenew(int? count = default) { if (count.HasValue) _implementor.AssertAreEqual(count.Value, RenewCount, $"{nameof(RenewCount)} is not equal."); @@ -244,7 +244,7 @@ public ServiceBusSessionMessageActionsAssertor AssertRenew(int? count = default) /// /// Asserts that no methods (with the exception of ) were invoked. /// - /// The to support fluent-style method-chaining. - public ServiceBusSessionMessageActionsAssertor AssertNone() => AssertStatus(ServiceBusMessageActionStatus.None); + /// The to support fluent-style method-chaining. + public WebJobsServiceBusSessionMessageActionsAssertor AssertNone() => AssertStatus(ServiceBusMessageActionStatus.None); } } \ No newline at end of file diff --git a/src/UnitTestEx/Functions/WorkerServiceBusMessageActionsAssertor.cs b/src/UnitTestEx/Functions/WorkerServiceBusMessageActionsAssertor.cs new file mode 100644 index 0000000..bef65c9 --- /dev/null +++ b/src/UnitTestEx/Functions/WorkerServiceBusMessageActionsAssertor.cs @@ -0,0 +1,171 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx + +using Azure.Messaging.ServiceBus; +using Microsoft.Azure.Functions.Worker; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnitTestEx.Abstractions; + +namespace UnitTestEx.Functions +{ + /// + /// Provides a test mock and assert verification. + /// + /// The . + public class WorkerServiceBusMessageActionsAssertor(TestFrameworkImplementor implementor) : ServiceBusMessageActions + { + private readonly TestFrameworkImplementor _implementor = implementor ?? throw new ArgumentNullException(nameof(implementor)); + + /// + /// Gets the . + /// + public ServiceBusMessageActionStatus Status { get; private set; } = ServiceBusMessageActionStatus.None; + + /// + /// Gets the reason where specified. + /// + public string? DeadLetterReason { get; private set; } + + /// + /// Gets the error description where specified. + /// + public string? DeadLetterErrorDescription { get; private set; } + + /// + /// Gets the properties of the message modified where specified. + /// + public IDictionary? PropertiesModified { get; private set; } + + /// + public override Task AbandonMessageAsync(ServiceBusReceivedMessage message, IDictionary? propertiesToModify = default, CancellationToken cancellationToken = default) + { + VerifyAndSetActionStatus(ServiceBusMessageActionStatus.Abandon); + Status = ServiceBusMessageActionStatus.Abandon; + PropertiesModified = propertiesToModify; + return Task.CompletedTask; + } + + /// + public override Task CompleteMessageAsync(ServiceBusReceivedMessage message, CancellationToken cancellationToken = default) + { + VerifyAndSetActionStatus(ServiceBusMessageActionStatus.Complete); + return Task.CompletedTask; + } + + /// + public override Task DeadLetterMessageAsync(ServiceBusReceivedMessage message, Dictionary? propertiesToModify, string? deadLetterReason, string? deadLetterErrorDescription = default!, CancellationToken cancellationToken = default) + { + VerifyAndSetActionStatus(ServiceBusMessageActionStatus.DeadLetter); + DeadLetterReason = deadLetterReason; + DeadLetterErrorDescription = deadLetterErrorDescription; + PropertiesModified = propertiesToModify; + return Task.CompletedTask; + } + + /// + public override Task DeferMessageAsync(ServiceBusReceivedMessage message, IDictionary? propertiesToModify = null, CancellationToken cancellationToken = default) + { + VerifyAndSetActionStatus(ServiceBusMessageActionStatus.Defer); + PropertiesModified = propertiesToModify; + return Task.CompletedTask; + } + + /// + /// Verifies action is a valid operation. + /// + private void VerifyAndSetActionStatus(ServiceBusMessageActionStatus status) + { + if (Status != ServiceBusMessageActionStatus.None) + throw new InvalidOperationException($"The {status}MessageAsync cannot be invoked after a previous {Status}."); + + if (status != ServiceBusMessageActionStatus.Renew) + Status = status; + } + + /// + /// Logs the result of the . + /// + internal void LogResult() + { + _implementor.WriteLine(""); + _implementor.WriteLine("MESSAGE ACTIONS >"); + _implementor.WriteLine($"Action: {Status}"); + + if (Status == ServiceBusMessageActionStatus.DeadLetter) + { + _implementor.WriteLine($"Reason: {DeadLetterReason ?? ""}"); + _implementor.WriteLine($"Description: {DeadLetterErrorDescription ?? ""}"); + } + + _implementor.WriteLine($"Properties modified{(PropertiesModified is null ? ": None." : " >")}"); + if (PropertiesModified != null) + { + foreach (var pm in PropertiesModified) + { + _implementor.WriteLine($" {pm.Key}: {pm.Value ?? ""}"); + } + } + } + + /// + /// Assert the status. + /// + private WorkerServiceBusMessageActionsAssertor AssertStatus(ServiceBusMessageActionStatus status) + { + if (!Status.Equals(status)) + _implementor.AssertAreEqual(status, Status, "ServiceBusMessageActions status is not equal."); + + return this; + } + + /// + /// Asserts that the was invoked. + /// + /// The to support fluent-style method-chaining. + public WorkerServiceBusMessageActionsAssertor AssertAbandon() => AssertStatus(ServiceBusMessageActionStatus.Abandon); + + /// + /// Asserts that the was invoked. + /// + /// The to support fluent-style method-chaining. + public WorkerServiceBusMessageActionsAssertor AssertComplete() => AssertStatus(ServiceBusMessageActionStatus.Complete); + + /// + /// Asserts that the was invoked. + /// + /// The to support fluent-style method-chaining. + public WorkerServiceBusMessageActionsAssertor AssertDefer() => AssertStatus(ServiceBusMessageActionStatus.Defer); + + /// + /// Asserts that the methods was invoked. + /// + /// Asserts that the resulting contains the specified content. + /// Asserts that the resulting contains the specified content. + /// The to support fluent-style method-chaining. + public WorkerServiceBusMessageActionsAssertor AssertDeadLetter(string? reasonContains = default, string? errorDescriptionContains = default) + { + AssertStatus(ServiceBusMessageActionStatus.DeadLetter); + if (!string.IsNullOrEmpty(reasonContains)) + { + if (string.IsNullOrEmpty(DeadLetterReason) || !DeadLetterReason.Contains(reasonContains, StringComparison.InvariantCultureIgnoreCase)) + _implementor.AssertFail($"Expected the {nameof(DeadLetterReason)} to contain: {reasonContains}"); + } + + if (!string.IsNullOrEmpty(errorDescriptionContains)) + { + if (string.IsNullOrEmpty(DeadLetterErrorDescription) || !DeadLetterErrorDescription.Contains(errorDescriptionContains, StringComparison.InvariantCultureIgnoreCase)) + _implementor.AssertFail($"Expected the {nameof(DeadLetterErrorDescription)} to contain: {errorDescriptionContains}"); + } + + return this; + } + + /// + /// Asserts that no methods were invoked. + /// + /// The to support fluent-style method-chaining. + public WorkerServiceBusMessageActionsAssertor AssertNone() => AssertStatus(ServiceBusMessageActionStatus.None); + } +} \ No newline at end of file diff --git a/src/UnitTestEx/Hosting/HostTesterBase.cs b/src/UnitTestEx/Hosting/HostTesterBase.cs index 2bd418a..f4b717f 100644 --- a/src/UnitTestEx/Hosting/HostTesterBase.cs +++ b/src/UnitTestEx/Hosting/HostTesterBase.cs @@ -49,10 +49,10 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) /// Orchestrates the execution of a method as described by the returning no result. /// /// The method execution expression. - /// The optional parameter to find. + /// The optional parameter (s) to find. /// Action to verify the method parameters prior to method invocation. /// The resulting exception if any and elapsed milliseconds. - protected async Task<(Exception? Exception, double ElapsedMilliseconds)> RunAsync(Expression> expression, Type? paramAttributeType, Action? onBeforeRun) + protected async Task<(Exception? Exception, double ElapsedMilliseconds)> RunAsync(Expression> expression, Type[]? paramAttributeTypes, Action? onBeforeRun) { TestSetUp.LogAutoSetUpOutputs(Implementor); @@ -68,9 +68,15 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) var le = Expression.Lambda>(ue); @params[i] = le.Compile().Invoke(); - if (paramAttribute == null && paramAttributeType != null) + if (paramAttribute == null && paramAttributeTypes != null) { - paramAttribute = (Attribute?)pis[i].GetCustomAttributes(paramAttributeType, false).FirstOrDefault()!; + for (int j = 0; j < paramAttributeTypes.Length; j++) + { + paramAttribute = (Attribute?)pis[i].GetCustomAttributes(paramAttributeTypes[j], false).FirstOrDefault()!; + if (paramAttribute != null) + break; + } + paramValue = @params[i]; } } @@ -103,10 +109,10 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) /// /// The result value . /// The method execution expression. - /// The optional parameter to find. + /// The optional parameter array to find. /// Action to verify the method parameters prior to method invocation. /// The resulting value, resulting exception if any, and elapsed milliseconds. - protected async Task<(TValue Result, Exception? Exception, double ElapsedMilliseconds)> RunAsync(Expression>> expression, Type? paramAttributeType, Action? onBeforeRun) + protected async Task<(TValue Result, Exception? Exception, double ElapsedMilliseconds)> RunAsync(Expression>> expression, Type[]? paramAttributeTypes, Action? onBeforeRun) { TestSetUp.LogAutoSetUpOutputs(Implementor); @@ -122,9 +128,15 @@ public class HostTesterBase(TesterBase owner, IServiceScope serviceScope) var le = Expression.Lambda>(ue); @params[i] = le.Compile().Invoke(); - if (paramAttribute == null && paramAttributeType != null) + if (paramAttribute == null && paramAttributeTypes != null) { - paramAttribute = (Attribute?)pis[i].GetCustomAttributes(paramAttributeType, false).FirstOrDefault()!; + for (int j = 0; j < paramAttributeTypes.Length; j++) + { + paramAttribute = (Attribute?)pis[i].GetCustomAttributes(paramAttributeTypes[j], false).FirstOrDefault()!; + if (paramAttribute != null) + break; + } + paramValue = @params[i]; } } diff --git a/src/UnitTestEx/UnitTestEx.csproj b/src/UnitTestEx/UnitTestEx.csproj index 506a47e..c11ce2f 100644 --- a/src/UnitTestEx/UnitTestEx.csproj +++ b/src/UnitTestEx/UnitTestEx.csproj @@ -16,28 +16,29 @@ + + - - - + + - + - + - + - + - + - + diff --git a/tests/UnitTestEx.Function/UnitTestEx.Function.csproj b/tests/UnitTestEx.Function/UnitTestEx.Function.csproj index 1458e33..a832d62 100644 --- a/tests/UnitTestEx.Function/UnitTestEx.Function.csproj +++ b/tests/UnitTestEx.Function/UnitTestEx.Function.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/UnitTestEx.IsolatedFunction/UnitTestEx.IsolatedFunction.csproj b/tests/UnitTestEx.IsolatedFunction/UnitTestEx.IsolatedFunction.csproj index 30507e1..c3cc00b 100644 --- a/tests/UnitTestEx.IsolatedFunction/UnitTestEx.IsolatedFunction.csproj +++ b/tests/UnitTestEx.IsolatedFunction/UnitTestEx.IsolatedFunction.csproj @@ -7,13 +7,13 @@ enable - - - - - - - + + + + + + + diff --git a/tests/UnitTestEx.MSTest.Test/UnitTestEx.MSTest.Test.csproj b/tests/UnitTestEx.MSTest.Test/UnitTestEx.MSTest.Test.csproj index d432719..8536dbf 100644 --- a/tests/UnitTestEx.MSTest.Test/UnitTestEx.MSTest.Test.csproj +++ b/tests/UnitTestEx.MSTest.Test/UnitTestEx.MSTest.Test.csproj @@ -24,9 +24,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/UnitTestEx.NUnit.Test/MockHttpClientTest.cs b/tests/UnitTestEx.NUnit.Test/MockHttpClientTest.cs index 059fc29..e93ade1 100644 --- a/tests/UnitTestEx.NUnit.Test/MockHttpClientTest.cs +++ b/tests/UnitTestEx.NUnit.Test/MockHttpClientTest.cs @@ -23,10 +23,10 @@ public async Task UriOnly_Single() var hc = mcf.GetHttpClient("XXX"); var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NotFound, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); res = await hc.GetAsync("products/xyz").ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NotFound, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); } [Test] @@ -37,10 +37,10 @@ public async Task UriOnly_Encoded() var hc = mcf.GetHttpClient("XXX"); var res = await hc.GetAsync("person/xyz?search=email eq \"bob@email.com\"").ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NotFound, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); res = await hc.GetAsync("person/xyz?search=email%20eq%20%22bob@email.com%22").ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NotFound, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); } [Test] @@ -53,10 +53,10 @@ public async Task UriOnly_Multi() var hc = mcf.GetHttpClient("XXX"); var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NotFound, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); res = await hc.GetAsync("products/abc").ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NoContent, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); } [Test] @@ -67,7 +67,7 @@ public async Task UriAndBody_String_Single() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsync("products/xyz", new StringContent("Bananas")).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); } [Test] @@ -80,10 +80,10 @@ public async Task UriAndBody_String_Multi() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsync("products/xyz", new StringContent("Bananas")).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); res = await hc.PostAsync("products/xyz", new StringContent("Apples")).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NoContent, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); } [Test] @@ -105,7 +105,7 @@ public async Task UriAndBody_Json_Single() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); } [Test] @@ -118,10 +118,10 @@ public async Task UriAndBody_Json_Multi() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); res = await hc.PostAsync("products/xyz", new StringContent("{ \"firstName\": \"Jenny\", \"lastName\": \"Browne\" }", Encoding.UTF8, MediaTypeNames.Application.Json)).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.OK, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.OK)); } [Test] @@ -134,8 +134,8 @@ public async Task UriAndBody_WithJsonResponse() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); - Assert.AreEqual("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); + Assert.That(await res.Content.ReadAsStringAsync().ConfigureAwait(false), Is.EqualTo("{\"first\":\"Bob\",\"last\":\"Jane\"}")); } [Test] @@ -148,8 +148,8 @@ public async Task UriAndBody_WithJsonResponse2() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); - Assert.AreEqual("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); + Assert.That(await res.Content.ReadAsStringAsync().ConfigureAwait(false), Is.EqualTo("{\"first\":\"Bob\",\"last\":\"Jane\"}")); } [Test] @@ -162,8 +162,8 @@ public async Task UriAndBody_WithJsonResponse3() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); - Assert.AreEqual("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); + Assert.That(await res.Content.ReadAsStringAsync().ConfigureAwait(false), Is.EqualTo("{\"first\":\"Bob\",\"last\":\"Jane\"}")); } [Test] @@ -181,7 +181,7 @@ public void VerifyMock_NotExecuted() } catch (MockHttpClientException mhcex) { - Assert.AreEqual("The request was invoked 0 times; expected AtLeastOnce. Request: POST https://d365test/products/xyz {\"firstName\":\"Bob\",\"lastName\":\"Jane\"} (application/json)", mhcex.Message); + Assert.That(mhcex.Message, Is.EqualTo("The request was invoked 0 times; expected AtLeastOnce. Request: POST https://d365test/products/xyz {\"firstName\":\"Bob\",\"lastName\":\"Jane\"} (application/json)")); } } @@ -200,14 +200,14 @@ public async Task VerifyMock_NotExecuted_Multi() { var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); mcf.VerifyAll(); Assert.Fail(); } catch (MockHttpClientException mhcex) { - Assert.AreEqual("The request was invoked 0 times; expected AtLeastOnce. Request: POST https://d365test/products/abc {\"firstName\":\"David\",\"lastName\":\"Jane\"} (application/json)", mhcex.Message); + Assert.That(mhcex.Message, Is.EqualTo("The request was invoked 0 times; expected AtLeastOnce. Request: POST https://d365test/products/abc {\"firstName\":\"David\",\"lastName\":\"Jane\"} (application/json)")); } } @@ -223,18 +223,18 @@ public async Task VerifyMock_NotExecuted_Times() { var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); mcf.VerifyAll(); Assert.Fail(); } catch (MockHttpClientException mhcex) { - Assert.AreEqual("The request was invoked 3 times; expected Exactly(2). Request: POST https://d365test/products/xyz {\"firstName\":\"Bob\",\"lastName\":\"Jane\"} (application/json)", mhcex.Message); + Assert.That(mhcex.Message, Is.EqualTo("The request was invoked 3 times; expected Exactly(2). Request: POST https://d365test/products/xyz {\"firstName\":\"Bob\",\"lastName\":\"Jane\"} (application/json)")); } } @@ -262,9 +262,9 @@ public async Task UriAndAnyBody() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); - Assert.AreEqual("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); - Assert.IsNotNull(res.RequestMessage); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); + Assert.That(await res.Content.ReadAsStringAsync().ConfigureAwait(false), Is.EqualTo("{\"first\":\"Bob\",\"last\":\"Jane\"}")); + Assert.That(res.RequestMessage, Is.Not.Null); Assert.ThrowsAsync(async () => await hc.SendAsync(new HttpRequestMessage(HttpMethod.Post, "products/xyz"))); } @@ -282,10 +282,10 @@ public async Task MockSequence_Body() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.OK, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.OK)); } [Test] @@ -301,10 +301,10 @@ public async Task MockSequence() var hc = mcf.GetHttpClient("XXX"); var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NotModified, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotModified)); res = await hc.GetAsync("products/xyz").ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.NotFound, res.StatusCode); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); } [Test] @@ -319,8 +319,8 @@ public async Task MockDelay() var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); sw.Stop(); - Assert.AreEqual(HttpStatusCode.NotFound, res.StatusCode); - Assert.IsTrue(sw.ElapsedMilliseconds >= 495); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + Assert.That(sw.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(495)); } [Test] @@ -339,14 +339,14 @@ public async Task MockSequenceDelay() var sw = Stopwatch.StartNew(); var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); sw.Stop(); - Assert.AreEqual(HttpStatusCode.NotModified, res.StatusCode); - Assert.IsTrue(sw.ElapsedMilliseconds >= 245); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotModified)); + Assert.That(sw.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(245)); sw.Restart(); res = await hc.GetAsync("products/xyz").ConfigureAwait(false); sw.Stop(); - Assert.AreEqual(HttpStatusCode.NotFound, res.StatusCode); - Assert.IsTrue(sw.ElapsedMilliseconds >= 95); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + Assert.That(sw.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(95)); } [Test] @@ -359,8 +359,8 @@ public async Task UriAndBody_WithXmlRequest() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsXmlAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); - Assert.AreEqual("BobJane", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); + Assert.That(await res.Content.ReadAsStringAsync().ConfigureAwait(false), Is.EqualTo("BobJane")); } [Test] @@ -373,8 +373,8 @@ public async Task UriAndBody_WithAnyTypeRequest() var hc = mcf.GetHttpClient("XXX"); var res = await hc.PostAsync("testing", new StringContent("--my--custom--format--", Encoding.UTF8, "application/custom-format")).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); - Assert.AreEqual("--ok--", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); + Assert.That(await res.Content.ReadAsStringAsync().ConfigureAwait(false), Is.EqualTo("--ok--")); } [Test] @@ -387,9 +387,9 @@ public async Task DefaultHttpClient() var hc = mcf.GetHttpClient(); var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.Accepted, res.StatusCode); - Assert.AreEqual("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); - Assert.IsNotNull(res.RequestMessage); + Assert.That(res.StatusCode, Is.EqualTo(HttpStatusCode.Accepted)); + Assert.That(await res.Content.ReadAsStringAsync().ConfigureAwait(false), Is.EqualTo("{\"first\":\"Bob\",\"last\":\"Jane\"}")); + Assert.That(res.RequestMessage, Is.Not.Null); Assert.ThrowsAsync(async () => await hc.SendAsync(new HttpRequestMessage(HttpMethod.Post, "products/xyz"))); } diff --git a/tests/UnitTestEx.NUnit.Test/Other/ExpectationsTest.cs b/tests/UnitTestEx.NUnit.Test/Other/ExpectationsTest.cs index 7225ecd..c7ef1ac 100644 --- a/tests/UnitTestEx.NUnit.Test/Other/ExpectationsTest.cs +++ b/tests/UnitTestEx.NUnit.Test/Other/ExpectationsTest.cs @@ -15,7 +15,7 @@ public void ExceptionSuccess_ExpectException_Any() var gt = GenericTester.Create().ExpectException().Any(); var ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertAsync(null, null))); - Assert.AreEqual("Expected an exception; however, the execution was successful.", ex.Message); + Assert.That(ex.Message, Is.EqualTo("Expected an exception; however, the execution was successful.")); ArrangerAssert(async () => await gt.ExpectationsArranger.AssertAsync(null, new DivideByZeroException())); } @@ -28,7 +28,7 @@ public void ExceptionSuccess_ExpectException_Message() ArrangerAssert(async () => await gt.ExpectationsArranger.AssertAsync(null, new DivideByZeroException("Error"))); var ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertAsync(null, new DivideByZeroException("not ok")))); - Assert.AreEqual("Expected Exception message 'error' is not contained within 'not ok'.", ex.Message); + Assert.That(ex.Message, Is.EqualTo("Expected Exception message 'error' is not contained within 'not ok'.")); } [Test] @@ -37,10 +37,10 @@ public void ExceptionSuccess_ExpectException_Type() var gt = GenericTester.Create().ExpectException().Type(); var ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertAsync(null, null))); - Assert.AreEqual("Expected an exception; however, the execution was successful.", ex.Message); + Assert.That(ex.Message, Is.EqualTo("Expected an exception; however, the execution was successful.")); ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertAsync(null, new NotSupportedException()))); - Assert.AreEqual("Expected Exception type 'DivideByZeroException' not equal to actual 'NotSupportedException'.", ex.Message); + Assert.That(ex.Message, Is.EqualTo("Expected Exception type 'DivideByZeroException' not equal to actual 'NotSupportedException'.")); ArrangerAssert(async () => await gt.ExpectationsArranger.AssertAsync(null, new DivideByZeroException())); } @@ -51,7 +51,7 @@ public void ExpectError_None() var gt = GenericTester.Create().ExpectError("No error will be raised."); var ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertAsync(null, null))); - Assert.AreEqual("Expected one or more errors; however, none were returned.", ex.Message); + Assert.That(ex.Message, Is.EqualTo("Expected one or more errors; however, none were returned.")); } [Test] @@ -61,10 +61,10 @@ public void ExpectValue_Simple() ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, "bob")); var ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, "jenny"))); - Assert.IsTrue(ex.Message.Contains("Value is not equal: \"bob\" != \"jenny\".")); + Assert.That(ex.Message.Contains("Value is not equal: \"bob\" != \"jenny\"."), Is.True); ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, null))); - Assert.IsTrue(ex.Message.Contains("Kind is not equal: String != Null.")); + Assert.That(ex.Message.Contains("Kind is not equal: String != Null."), Is.True); } [Test] @@ -74,10 +74,10 @@ public void ExpectValue_WithFunc() ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, "bob")); var ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, "jenny"))); - Assert.IsTrue(ex.Message.Contains("Value is not equal: \"bob\" != \"jenny\".")); + Assert.That(ex.Message.Contains("Value is not equal: \"bob\" != \"jenny\"."), Is.True); ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, null))); - Assert.IsTrue(ex.Message.Contains("Kind is not equal: String != Null.")); + Assert.That(ex.Message.Contains("Kind is not equal: String != Null."), Is.True); } [Test] @@ -89,7 +89,7 @@ public void ExpectValueComplex() ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, new { Id = 88, Name = "bob" })); var ex = Assert.Throws(() => ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, new { Id = 99, Name = "bob" }))); - Assert.IsTrue(ex.Message.Contains("Path '$.id': Value is not equal: 88 != 99.")); + Assert.That(ex.Message.Contains("Path '$.id': Value is not equal: 88 != 99."), Is.True); gt = GenericTester.CreateFor>().ExpectValue(new Entity { Id = 88, Name = "bob" }, "id"); ArrangerAssert(async () => await gt.ExpectationsArranger.AssertValueAsync(null, new { Id = 99, Name = "bob" })); diff --git a/tests/UnitTestEx.NUnit.Test/Other/OneOffTestSetUpTest.cs b/tests/UnitTestEx.NUnit.Test/Other/OneOffTestSetUpTest.cs index 23e32c5..ab59d1d 100644 --- a/tests/UnitTestEx.NUnit.Test/Other/OneOffTestSetUpTest.cs +++ b/tests/UnitTestEx.NUnit.Test/Other/OneOffTestSetUpTest.cs @@ -15,7 +15,7 @@ public class OneOffTestSetUpTest [Test] public void SetUp_SetDefaultUserName() { - Assert.AreEqual("Luke", TestSetUp.Default.DefaultUserName); + Assert.That(TestSetUp.Default.DefaultUserName, Is.EqualTo("Luke")); } [Test] @@ -23,7 +23,7 @@ public void TesterExtensions_ApiTester() { using var test = ApiTester.Create(); var bs = test.Services.GetService(); - Assert.IsNotNull(bs); + Assert.That(bs, Is.Not.Null); } [Test] @@ -31,7 +31,7 @@ public void TesterExtensions_FunctionTester() { using var test = FunctionTester.Create(); var bs = test.Services.GetService(); - Assert.IsNotNull(bs); + Assert.That(bs, Is.Not.Null); } [Test] @@ -39,7 +39,7 @@ public void TesterExtensions_GenericTester() { using var test = GenericTester.Create(); var bs = test.Services.GetService(); - Assert.IsNotNull(bs); + Assert.That(bs, Is.Not.Null); } } diff --git a/tests/UnitTestEx.NUnit.Test/PersonControllerTest.cs b/tests/UnitTestEx.NUnit.Test/PersonControllerTest.cs index cd41ca8..90882fa 100644 --- a/tests/UnitTestEx.NUnit.Test/PersonControllerTest.cs +++ b/tests/UnitTestEx.NUnit.Test/PersonControllerTest.cs @@ -177,7 +177,7 @@ public void Update_Test5_ExpectationFailure() .Run(c => c.Update(1, new Person { FirstName = null, LastName = null })); }); - Assert.IsTrue(ex.Message.Contains("Error: First name is requiredx.")); + Assert.That(ex.Message.Contains("Error: First name is requiredx."), Is.True); } [Test] diff --git a/tests/UnitTestEx.NUnit.Test/ProductControllerTest.cs b/tests/UnitTestEx.NUnit.Test/ProductControllerTest.cs index 9bc1845..18fce03 100644 --- a/tests/UnitTestEx.NUnit.Test/ProductControllerTest.cs +++ b/tests/UnitTestEx.NUnit.Test/ProductControllerTest.cs @@ -87,8 +87,8 @@ public void ServiceProvider() .Services.GetService().CreateClient("XXX"); var r = hc.GetAsync("test").Result; - Assert.IsNotNull(r); - Assert.AreEqual("test output", r.Content.ReadAsStringAsync().Result); + Assert.That(r, Is.Not.Null); + Assert.That(r.Content.ReadAsStringAsync().Result, Is.EqualTo("test output")); } [Test] @@ -96,10 +96,10 @@ public void Configuration() { using var test = ApiTester.Create(); var cv = test.Configuration.GetValue("SpecialKey"); - Assert.AreEqual("VerySpecialValue", cv); + Assert.That(cv, Is.EqualTo("VerySpecialValue")); cv = test.Configuration.GetValue("OtherKey"); - Assert.AreEqual("OtherValue", cv); + Assert.That(cv, Is.EqualTo("OtherValue")); } [Test] diff --git a/tests/UnitTestEx.NUnit.Test/ServiceBusFunctionTest.cs b/tests/UnitTestEx.NUnit.Test/ServiceBusFunctionTest.cs index 44887f2..056bb46 100644 --- a/tests/UnitTestEx.NUnit.Test/ServiceBusFunctionTest.cs +++ b/tests/UnitTestEx.NUnit.Test/ServiceBusFunctionTest.cs @@ -110,7 +110,7 @@ public void ServiceBusMessage_AssertActions() { using var test = FunctionTester.Create(); var msg = test.CreateServiceBusMessageFromValue(new Person { FirstName = null, LastName = "Smith" }); - var act = test.CreateServiceBusMessageActions(); + var act = test.CreateWebJobsServiceBusMessageActions(); test.ServiceBusTrigger() .Run(f => f.Run3(msg, act, test.Logger)) @@ -130,8 +130,8 @@ public void ServiceProvider() .Services.GetService().CreateClient("XXX"); var r = hc.GetAsync("test").Result; - Assert.IsNotNull(r); - Assert.AreEqual("test output", r.Content.ReadAsStringAsync().Result); + Assert.That(r, Is.Not.Null); + Assert.That(r.Content.ReadAsStringAsync().Result, Is.EqualTo("test output")); } [Test] @@ -139,7 +139,7 @@ public void Configuration() { using var test = FunctionTester.Create(); var cv = test.Configuration.GetValue("SpecialKey"); - Assert.AreEqual("VerySpecialValue", cv); + Assert.That(cv, Is.EqualTo("VerySpecialValue")); } } } \ No newline at end of file diff --git a/tests/UnitTestEx.NUnit.Test/UnitTestEx.NUnit.Test.csproj b/tests/UnitTestEx.NUnit.Test/UnitTestEx.NUnit.Test.csproj index b5168ea..b4001e0 100644 --- a/tests/UnitTestEx.NUnit.Test/UnitTestEx.NUnit.Test.csproj +++ b/tests/UnitTestEx.NUnit.Test/UnitTestEx.NUnit.Test.csproj @@ -23,8 +23,12 @@ - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/tests/UnitTestEx.Xunit.Test/MockHttpClientTest.cs b/tests/UnitTestEx.Xunit.Test/MockHttpClientTest.cs index 6cdb330..879771a 100644 --- a/tests/UnitTestEx.Xunit.Test/MockHttpClientTest.cs +++ b/tests/UnitTestEx.Xunit.Test/MockHttpClientTest.cs @@ -23,10 +23,10 @@ public async Task UriOnly_Single() mcf.CreateClient("XXX", new Uri("https://d365test")).Request(HttpMethod.Get, "products/xyz").Respond.With(HttpStatusCode.NotFound); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); + var res = await hc.GetAsync("products/xyz"); Assert.Equal(HttpStatusCode.NotFound, res.StatusCode); - res = await hc.GetAsync("products/xyz").ConfigureAwait(false); + res = await hc.GetAsync("products/xyz"); Assert.Equal(HttpStatusCode.NotFound, res.StatusCode); } @@ -39,10 +39,10 @@ public async Task UriOnly_Multi() mc.Request(HttpMethod.Get, "products/abc").Respond.With(HttpStatusCode.NoContent); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); + var res = await hc.GetAsync("products/xyz"); Assert.Equal(HttpStatusCode.NotFound, res.StatusCode); - res = await hc.GetAsync("products/abc").ConfigureAwait(false); + res = await hc.GetAsync("products/abc"); Assert.Equal(HttpStatusCode.NoContent, res.StatusCode); } @@ -53,7 +53,7 @@ public async Task UriAndBody_String_Single() mcf.CreateClient("XXX", new Uri("https://d365test")).Request(HttpMethod.Post, "products/xyz").WithBody("Bananas").Respond.With(HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsync("products/xyz", new StringContent("Bananas")).ConfigureAwait(false); + var res = await hc.PostAsync("products/xyz", new StringContent("Bananas")); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); } @@ -66,22 +66,22 @@ public async Task UriAndBody_String_Multi() mc.Request(HttpMethod.Post, "products/xyz").WithBody("Apples").Respond.With(HttpStatusCode.NoContent); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsync("products/xyz", new StringContent("Bananas")).ConfigureAwait(false); + var res = await hc.PostAsync("products/xyz", new StringContent("Bananas")); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - res = await hc.PostAsync("products/xyz", new StringContent("Apples")).ConfigureAwait(false); + res = await hc.PostAsync("products/xyz", new StringContent("Apples")); Assert.Equal(HttpStatusCode.NoContent, res.StatusCode); } [Fact] - public void UriAndBody_Invalid() + public async Task UriAndBody_Invalid() { var mcf = CreateMockHttpClientFactory(); mcf.CreateClient("XXX", new Uri("https://d365test")).Request(HttpMethod.Post, "products/xyz").WithBody("Bananas").Respond.With(HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - Assert.ThrowsAsync(() => hc.PostAsync("products/xyz", new StringContent("Apples"))); + await Assert.ThrowsAsync(() => hc.PostAsync("products/xyz", new StringContent("Apples"))); } [Fact] @@ -91,7 +91,7 @@ public async Task UriAndBody_Json_Single() mcf.CreateClient("XXX", new Uri("https://d365test")).Request(HttpMethod.Post, "products/xyz").WithJsonBody(new Person { FirstName = "Bob", LastName = "Jane" }).Respond.With(HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); } @@ -104,10 +104,10 @@ public async Task UriAndBody_Json_Multi() mc.Request(HttpMethod.Post, "products/xyz").WithJsonBody(new Person { FirstName = "Jenny", LastName = "Browne" }).Respond.With(HttpStatusCode.OK); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - res = await hc.PostAsync("products/xyz", new StringContent("{ \"firstName\": \"Jenny\", \"lastName\": \"Browne\" }", Encoding.UTF8, MediaTypeNames.Application.Json)).ConfigureAwait(false); + res = await hc.PostAsync("products/xyz", new StringContent("{ \"firstName\": \"Jenny\", \"lastName\": \"Browne\" }", Encoding.UTF8, MediaTypeNames.Application.Json)); Assert.Equal(HttpStatusCode.OK, res.StatusCode); } @@ -120,9 +120,9 @@ public async Task UriAndBody_WithJsonResponse() .Respond.WithJson(new Person2 { First = "Bob", Last = "Jane" }, HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - Assert.Equal("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.Equal("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync()); } [Fact] @@ -134,9 +134,9 @@ public async Task UriAndBody_WithJsonResponse2() .Respond.WithJson("{\"first\":\"Bob\",\"last\":\"Jane\"}", HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - Assert.Equal("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.Equal("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync()); } [Fact] @@ -148,9 +148,9 @@ public async Task UriAndBody_WithJsonResponse3() .Respond.WithJsonResource("MockHttpClientTest-UriAndBody_WithJsonResponse3.json", HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - Assert.Equal("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.Equal("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync()); } [Fact] @@ -186,7 +186,7 @@ public async Task VerifyMock_NotExecuted_Multi() try { var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); mcf.VerifyAll(); @@ -209,11 +209,11 @@ public async Task VerifyMock_NotExecuted_Times() try { var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); mcf.VerifyAll(); @@ -234,7 +234,7 @@ public async Task VerifyMock_Executed() .Respond.WithJsonResource("MockHttpClientTest-UriAndBody_WithJsonResponse3.json", HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); mcf.VerifyAll(); } @@ -248,9 +248,9 @@ public async Task UriAndAnyBody() .Respond.WithJson("{\"first\":\"Bob\",\"last\":\"Jane\"}", HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - Assert.Equal("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.Equal("{\"first\":\"Bob\",\"last\":\"Jane\"}", await res.Content.ReadAsStringAsync()); await Assert.ThrowsAsync(async () => await hc.SendAsync(new HttpRequestMessage(HttpMethod.Post, "products/xyz"))); } @@ -267,10 +267,10 @@ public async Task MockSequence_Body() }); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + res = await hc.PostAsJsonAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.OK, res.StatusCode); } @@ -287,10 +287,10 @@ public async Task MockSequence() }); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); + var res = await hc.GetAsync("products/xyz"); Assert.Equal(HttpStatusCode.NotModified, res.StatusCode); - res = await hc.GetAsync("products/xyz").ConfigureAwait(false); + res = await hc.GetAsync("products/xyz"); Assert.Equal(HttpStatusCode.NotFound, res.StatusCode); } @@ -303,7 +303,7 @@ public async Task MockDelay() var hc = mcf.GetHttpClient("XXX"); var sw = Stopwatch.StartNew(); - var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); + var res = await hc.GetAsync("products/xyz"); sw.Stop(); Assert.Equal(HttpStatusCode.NotFound, res.StatusCode); @@ -324,13 +324,13 @@ public async Task MockSequenceDelay() var hc = mcf.GetHttpClient("XXX"); var sw = Stopwatch.StartNew(); - var res = await hc.GetAsync("products/xyz").ConfigureAwait(false); + var res = await hc.GetAsync("products/xyz"); sw.Stop(); Assert.Equal(HttpStatusCode.NotModified, res.StatusCode); Assert.True(sw.ElapsedMilliseconds >= 245, $"Actual elapsed milliseconds {sw.ElapsedMilliseconds}."); sw.Restart(); - res = await hc.GetAsync("products/xyz").ConfigureAwait(false); + res = await hc.GetAsync("products/xyz"); sw.Stop(); Assert.Equal(HttpStatusCode.NotFound, res.StatusCode); Assert.True(sw.ElapsedMilliseconds >= 95, $"Actual elapsed milliseconds {sw.ElapsedMilliseconds}."); @@ -345,9 +345,9 @@ public async Task UriAndBody_WithXmlRequest() .Respond.With(new StringContent("BobJane", Encoding.UTF8, MediaTypeNames.Application.Xml), HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsXmlAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }).ConfigureAwait(false); + var res = await hc.PostAsXmlAsync("products/xyz", new Person { LastName = "Jane", FirstName = "Bob" }); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - Assert.Equal("BobJane", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.Equal("BobJane", await res.Content.ReadAsStringAsync()); } [Fact] @@ -359,9 +359,9 @@ public async Task UriAndBody_WithAnyTypeRequest() .Respond.With(new StringContent("--ok--", Encoding.UTF8, "application/custom-format"), HttpStatusCode.Accepted); var hc = mcf.GetHttpClient("XXX"); - var res = await hc.PostAsync("testing", new StringContent("--my--custom--format--", Encoding.UTF8, "application/custom-format")).ConfigureAwait(false); + var res = await hc.PostAsync("testing", new StringContent("--my--custom--format--", Encoding.UTF8, "application/custom-format")); Assert.Equal(HttpStatusCode.Accepted, res.StatusCode); - Assert.Equal("--ok--", await res.Content.ReadAsStringAsync().ConfigureAwait(false)); + Assert.Equal("--ok--", await res.Content.ReadAsStringAsync()); } } } \ No newline at end of file diff --git a/tests/UnitTestEx.Xunit.Test/ProductControllerTest.cs b/tests/UnitTestEx.Xunit.Test/ProductControllerTest.cs index fab0a8b..97bb013 100644 --- a/tests/UnitTestEx.Xunit.Test/ProductControllerTest.cs +++ b/tests/UnitTestEx.Xunit.Test/ProductControllerTest.cs @@ -2,6 +2,7 @@ using System; using System.Net; using System.Net.Http; +using System.Threading.Tasks; using UnitTestEx.Api; using UnitTestEx.Api.Controllers; using Xunit; @@ -43,7 +44,7 @@ public void Success() } [Fact] - public void ServiceProvider() + public async Task ServiceProvider() { var mcf = CreateMockHttpClientFactory(); mcf.CreateClient("XXX", new Uri("https://somesys")).Request(HttpMethod.Get, "test").Respond.With("test output"); @@ -52,9 +53,9 @@ public void ServiceProvider() var hc = test.ReplaceHttpClientFactory(mcf) .Services.GetService().CreateClient("XXX"); - var r = hc.GetAsync("test").Result; + var r = await hc.GetAsync("test"); Assert.NotNull(r); - Assert.Equal("test output", r.Content.ReadAsStringAsync().Result); + Assert.Equal("test output", await r.Content.ReadAsStringAsync()); } } } \ No newline at end of file diff --git a/tests/UnitTestEx.Xunit.Test/ServiceBusFunctionTest.cs b/tests/UnitTestEx.Xunit.Test/ServiceBusFunctionTest.cs index d1c4f6a..4dda0d6 100644 --- a/tests/UnitTestEx.Xunit.Test/ServiceBusFunctionTest.cs +++ b/tests/UnitTestEx.Xunit.Test/ServiceBusFunctionTest.cs @@ -3,6 +3,7 @@ using System.Net; using System.Net.Http; using System.Text.Json; +using System.Threading.Tasks; using UnitTestEx.Function; using UnitTestEx.Json; using Xunit; @@ -107,7 +108,7 @@ public void ServiceBusMessage_ThrowsException() } [Fact] - public void ServiceProvider() + public async Task ServiceProvider() { var mcf = CreateMockHttpClientFactory(); mcf.CreateClient("XXX", new Uri("https://somesys")).Request(HttpMethod.Get, "test").Respond.With("test output"); @@ -116,9 +117,9 @@ public void ServiceProvider() var hc = test.ReplaceHttpClientFactory(mcf) .Services.GetService().CreateClient("XXX"); - var r = hc.GetAsync("test").Result; + var r = await hc.GetAsync("test"); Assert.NotNull(r); - Assert.Equal("test output", r.Content.ReadAsStringAsync().Result); + Assert.Equal("test output", await r.Content.ReadAsStringAsync()); } } } \ No newline at end of file diff --git a/tests/UnitTestEx.Xunit.Test/UnitTestEx.Xunit.Test.csproj b/tests/UnitTestEx.Xunit.Test/UnitTestEx.Xunit.Test.csproj index 237c7dc..719ff71 100644 --- a/tests/UnitTestEx.Xunit.Test/UnitTestEx.Xunit.Test.csproj +++ b/tests/UnitTestEx.Xunit.Test/UnitTestEx.Xunit.Test.csproj @@ -24,9 +24,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all From bed76319a5e5b28277a498e4397b7a7aca383783 Mon Sep 17 00:00:00 2001 From: Eric Sibly Date: Wed, 10 Jan 2024 17:28:23 -0800 Subject: [PATCH 2/3] Update version. --- Common.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Common.targets b/Common.targets index b16526d..10e6c6d 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 4.0.2 + 4.1.0 preview Avanade Avanade From 95b597581ad18d327ca3b6368e31e0bb3faef993 Mon Sep 17 00:00:00 2001 From: Eric Sibly Date: Thu, 11 Jan 2024 08:04:27 -0800 Subject: [PATCH 3/3] Tweak README. --- README.md | 10 +++++++--- tests/UnitTestEx.IsolatedFunction/HttpFunction.cs | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6c34f84..d1baf35 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ _UnitTestEx_ provides [.NET testing](https://docs.microsoft.com/en-us/dotnet/core/testing/) extensions to the most popular testing frameworks: [MSTest](https://github.com/Microsoft/testfx-docs), [NUnit](https://nunit.org/) and [Xunit](https://xunit.net/). -The scenarios that _UnitTestEx_ looks to address is the end-to-end unit-style testing of the following whereby the capabilities look to adhere to the AAA pattern of unit testing; Arrange, Act and Assert. +The scenarios that _UnitTestEx_ looks to address is the end-to-end unit-style testing of the following whereby the capabilities look to adhere to the _AAA_ pattern of unit testing; Arrange, Act and Assert. - [API Controller](#API-Controller) - [HTTP-triggered Azure Function](#HTTP-triggered-Azure-Function) @@ -32,7 +32,7 @@ The included [change log](CHANGELOG.md) details all key changes per published ve ## API Controller -This leverages the [`WebApplicationFactory`](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests) (WAF) as a means to host a test server in process to invoke APIs directly using HTTP requests. This has the benefit of validating the HTTP pipeline and all Dependency Injection (DI) configuration within. External system interactions can be mocked accordingly. +Leverages the [`WebApplicationFactory`](https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests) (WAF) as a means to host a test server in process to invoke APIs directly using HTTP requests. This has the benefit of validating the HTTP pipeline and all Dependency Injection (DI) configuration within. External system interactions can be mocked accordingly. _UnitTestEx_ encapsulates the `WebApplicationFactory` providing a simple means to arrange the input, execute (act), and assert the response. The following is an [example](./tests/UnitTestEx.NUnit.Test/ProductControllerTest.cs). @@ -62,13 +62,15 @@ test.ReplaceHttpClientFactory(mcf) .Assert(new { id = "Abc", description = "A blue carrot" }); ``` +Both the [_Isolated worker model_](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide) and [_In-process model_](https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) are supported. +
## Service Bus-trigger Azure Function As above, there is currently no easy means to integration (in-process) test Azure functions that rely on the [Azure Service Bus](https://azure.microsoft.com/en-us/services/service-bus/). _UnitTestEx_ looks to emulate by self-hosting the function, managing Dependency Injection (DI) configuration, and invocation of the specified method and verifies usage of the [`ServiceBusTriggerAttribute`](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus-trigger?tabs=csharp). -The following is an [example](./tests/UnitTestEx.NUnit.Test/ServiceBusFunctionTest.cs) of invoking the function method directly passing in a `ServiceBusReceivedMessage` created using `test.CreateServiceBusMessageFromValue` (this creates a message as if coming from Service Bus). +The following is an [example](./tests/UnitTestEx.NUnit.Test/ServiceBusFunctionTest.cs) of invoking the function method directly passing in a `ServiceBusReceivedMessage` created using `test.CreateServiceBusMessageFromValue` (this creates a message as if coming from Azure Service Bus). ``` csharp using var test = FunctionTester.Create(); @@ -78,6 +80,8 @@ test.ReplaceHttpClientFactory(mcf) .AssertSuccess(); ``` +Both the [_Isolated worker model_](https://learn.microsoft.com/en-us/azure/azure-functions/dotnet-isolated-process-guide) and [_In-process model_](https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-class-library) are supported. +
## Generic Azure Function Type diff --git a/tests/UnitTestEx.IsolatedFunction/HttpFunction.cs b/tests/UnitTestEx.IsolatedFunction/HttpFunction.cs index 3c3a9d9..f75a579 100644 --- a/tests/UnitTestEx.IsolatedFunction/HttpFunction.cs +++ b/tests/UnitTestEx.IsolatedFunction/HttpFunction.cs @@ -27,4 +27,4 @@ public HttpResponseData Run([HttpTrigger(AuthorizationLevel.Function, "get", "po return response; } } -} +} \ No newline at end of file