diff --git a/CHANGELOG.md b/CHANGELOG.md index 245c826..bc53314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Represents the **NuGet** versions. +## v5.9.0 +- *Enhancement:* Added `WithGenericTester` (_MSTest_ and _NUnit_ only) class to enable class-level generic tester usage versus one-off. +- *Enhancement:* Added `TesterBase.UseScopedTypeSetUp()` to enable a function that will be executed directly before each `ScopedTypeTester{TService}` is instantiated to allow standardized/common set up to occur. + ## v5.8.0 - *Enhancement:* Extended the `MockHttpClientResponse.With*` methods to support optional _media type_ parameter to enable specification of the `Content-Type` header value. - *Enhancement:* Added `HttpResponseMessageAssertor.AssertContentTypeProblemJson` to enable asserting that the content type is `application/problem+json`. diff --git a/Common.targets b/Common.targets index ad02b2d..15e2445 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 5.8.0 + 5.9.0 preview Avanade Avanade diff --git a/src/UnitTestEx.MSTest/WithGenericTester.cs b/src/UnitTestEx.MSTest/WithGenericTester.cs new file mode 100644 index 0000000..63b0bdb --- /dev/null +++ b/src/UnitTestEx.MSTest/WithGenericTester.cs @@ -0,0 +1,51 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx + +using System; +using UnitTestEx.Generic; + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace UnitTestEx +#pragma warning restore IDE0130 // Namespace does not match folder structure +{ + /// + /// Provides a shared to enable usage of the same underlying instance across multiple tests. + /// + /// The generic startup . + /// Implements to automatically dispose of the instance to release all resources. + /// Be aware that using may result in cross-test contamination. + public abstract class WithGenericTester : IDisposable where TEntryPoint : class + { + private bool _disposed; + private GenericTester? _genericTester = GenericTester.Create(); + + /// + /// Gets the underlying for testing. + /// + public GenericTester Test => _genericTester ?? throw new ObjectDisposedException(nameof(Test)); + + /// + /// Dispose of all resources. + /// + /// Indicates whether from the . + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _genericTester?.Dispose(); + _genericTester = null; + } + + _disposed = true; + } + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/UnitTestEx.NUnit/WithGenericTester.cs b/src/UnitTestEx.NUnit/WithGenericTester.cs new file mode 100644 index 0000000..63b0bdb --- /dev/null +++ b/src/UnitTestEx.NUnit/WithGenericTester.cs @@ -0,0 +1,51 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/UnitTestEx + +using System; +using UnitTestEx.Generic; + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace UnitTestEx +#pragma warning restore IDE0130 // Namespace does not match folder structure +{ + /// + /// Provides a shared to enable usage of the same underlying instance across multiple tests. + /// + /// The generic startup . + /// Implements to automatically dispose of the instance to release all resources. + /// Be aware that using may result in cross-test contamination. + public abstract class WithGenericTester : IDisposable where TEntryPoint : class + { + private bool _disposed; + private GenericTester? _genericTester = GenericTester.Create(); + + /// + /// Gets the underlying for testing. + /// + public GenericTester Test => _genericTester ?? throw new ObjectDisposedException(nameof(Test)); + + /// + /// Dispose of all resources. + /// + /// Indicates whether from the . + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _genericTester?.Dispose(); + _genericTester = null; + } + + _disposed = true; + } + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/UnitTestEx/Abstractions/TesterBaseT.cs b/src/UnitTestEx/Abstractions/TesterBaseT.cs index 525a65b..d88e470 100644 --- a/src/UnitTestEx/Abstractions/TesterBaseT.cs +++ b/src/UnitTestEx/Abstractions/TesterBaseT.cs @@ -5,6 +5,7 @@ using Moq; using System; using System.Collections.Generic; +using System.Diagnostics.Metrics; using System.Runtime.CompilerServices; using System.Threading.Tasks; using UnitTestEx.Hosting; @@ -18,6 +19,8 @@ namespace UnitTestEx.Abstractions /// The to support inheriting fluent-style method-chaining. public abstract class TesterBase : TesterBase where TSelf : TesterBase { + private Func? _scopedSetUpAsync; + /// /// Initializes a new instance of the class. /// @@ -116,6 +119,17 @@ public TSelf UseAdditionalConfiguration(IEnumerableUsage will result in a . public TSelf UseAdditionalConfiguration(string key, string? value) => UseAdditionalConfiguration([new KeyValuePair(key, value)]); + /// + /// Updates (replaces) the function that will be executed directly before each is instantiated to allow standardized/common set up to occur. + /// + /// The set-up function. + /// The to support fluent-style method-chaining. + public TSelf UseScopedTypeSetUp(Func setupAsync) + { + _scopedSetUpAsync = setupAsync; + return (TSelf)this; + } + /// /// Resets the underlying host to instantiate a new instance. /// @@ -426,6 +440,7 @@ public TSelf ScopedType(Action> scopedTeste ArgumentNullException.ThrowIfNull(scopedTester); using var scope = HostExecutionWrapper(Services.CreateScope); + InvokeScopedTesterSetUp(scope); var tester = new ScopedTypeTester(this, scope.ServiceProvider, scope.ServiceProvider.CreateInstance(serviceKey)); scopedTester(tester); return (TSelf)this; @@ -442,6 +457,7 @@ public TSelf ScopedType(Func serviceFactor { ArgumentNullException.ThrowIfNull(scopedTester); using var scope = HostExecutionWrapper(Services.CreateScope); + InvokeScopedTesterSetUp(scope); var tester = new ScopedTypeTester(this, scope.ServiceProvider, serviceFactory(scope.ServiceProvider)); scopedTester(tester); return (TSelf)this; @@ -462,6 +478,7 @@ public TSelf ScopedType(Func, Task> scopedT ArgumentNullException.ThrowIfNull(scopedTesterAsync); using var scope = HostExecutionWrapper(Services.CreateScope); + InvokeScopedTesterSetUp(scope); var tester = new ScopedTypeTester(this, scope.ServiceProvider, scope.ServiceProvider.CreateInstance(serviceKey)); scopedTesterAsync(tester).GetAwaiter().GetResult(); return (TSelf)this; @@ -481,6 +498,7 @@ public TSelf ScopedType(Func serviceFactor { ArgumentNullException.ThrowIfNull(scopedTesterAsync); using var scope = HostExecutionWrapper(Services.CreateScope); + InvokeScopedTesterSetUp(scope); var tester = new ScopedTypeTester(this, scope.ServiceProvider, serviceFactory(scope.ServiceProvider)); scopedTesterAsync(tester).GetAwaiter().GetResult(); return (TSelf)this; @@ -500,6 +518,7 @@ public TSelf ScopedType(Func, ValueTask> sc ArgumentNullException.ThrowIfNull(scopedTesterAsync); using var scope = HostExecutionWrapper(Services.CreateScope); + InvokeScopedTesterSetUp(scope); var tester = new ScopedTypeTester(this, scope.ServiceProvider, scope.ServiceProvider.CreateInstance(serviceKey)); scopedTesterAsync(tester).AsTask().GetAwaiter().GetResult(); return (TSelf)this; @@ -517,11 +536,21 @@ public TSelf ScopedType(Func serviceFactor { ArgumentNullException.ThrowIfNull(scopedTesterAsync); using var scope = HostExecutionWrapper(Services.CreateScope); + InvokeScopedTesterSetUp(scope); var tester = new ScopedTypeTester(this, scope.ServiceProvider, serviceFactory(scope.ServiceProvider)); scopedTesterAsync(tester).AsTask().GetAwaiter().GetResult(); return (TSelf)this; } #endif + + /// + /// Executes the scoped tester set up. + /// + private void InvokeScopedTesterSetUp(IServiceScope? scope) + { + if (scope is not null && _scopedSetUpAsync is not null) + _scopedSetUpAsync(scope.ServiceProvider).GetAwaiter().GetResult(); + } } } \ No newline at end of file diff --git a/tests/UnitTestEx.MSTest.Test/Other/WithGenericTesterTest.cs b/tests/UnitTestEx.MSTest.Test/Other/WithGenericTesterTest.cs new file mode 100644 index 0000000..5d56e80 --- /dev/null +++ b/tests/UnitTestEx.MSTest.Test/Other/WithGenericTesterTest.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTestEx.MSTest.Test.Other +{ + [TestClass] + public class WithGenericTesterTest : WithGenericTester + { + [TestMethod] + public void Run_Success() + { + var c = Test.Services.GetService(); + + Test.Run(() => 1) + .AssertSuccess() + .AssertValue(1); + } + + [TestMethod] + public void Run_Success_AssertJSON() + { + Test.Run(() => 1) + .AssertSuccess() + .AssertJson("1"); + } + } +} \ No newline at end of file diff --git a/tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs b/tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs index 891da67..04d18ba 100644 --- a/tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs +++ b/tests/UnitTestEx.NUnit.Test/Other/GenericTest.cs @@ -110,6 +110,9 @@ public class EntryPoint //public void ConfigureServices(IServiceCollection services) { } - public void ConfigureApplication(IHostApplicationBuilder builder) => builder.Services.AddSingleton(); + public void ConfigureApplication(IHostApplicationBuilder builder) + { + builder.Services.AddSingleton(); + } } } \ No newline at end of file diff --git a/tests/UnitTestEx.NUnit.Test/Other/WithGenericTesterTest.cs b/tests/UnitTestEx.NUnit.Test/Other/WithGenericTesterTest.cs new file mode 100644 index 0000000..a01f8f6 --- /dev/null +++ b/tests/UnitTestEx.NUnit.Test/Other/WithGenericTesterTest.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace UnitTestEx.NUnit.Test.Other +{ + public class WithGenericTesterTest : WithGenericTester + { + [Test] + public void Run_Success() + { + var c = Test.Services.GetService(); + + Test.Run(() => 1) + .AssertSuccess() + .AssertValue(1); + } + + [Test] + public void Run_Success_AssertJSON() + { + Test.Run(() => 1) + .AssertSuccess() + .AssertJson("1"); + } + } +} \ No newline at end of file