diff --git a/docs/input/cli/commandApp.md b/docs/input/cli/commandApp.md index c003f60..f940059 100644 --- a/docs/input/cli/commandApp.md +++ b/docs/input/cli/commandApp.md @@ -74,6 +74,9 @@ return app.Run(args); `TypeRegistrar` is a custom class that must be created by the user. This [example using `Microsoft.Extensions.DependencyInjection` as the container](https://github.com/spectreconsole/spectre.console/tree/main/examples/Cli/Injection) provides an example `TypeRegistrar` and `TypeResolver` that can be added to your application with small adjustments for your DI container. +Hint: If you do write your own implementation of `TypeRegistrar` and `TypeResolver` and you have some form of unit tests in place for your project, +there is a utility `TypeRegistrarBaseTests` available that can be used to ensure your implementations adhere to the required implementation. Simply call `TypeRegistrarBaseTests.RunAllTests()` and expect no `TypeRegistrarBaseTests.TestFailedException` to be thrown. + ## Interception `CommandApp` also provides a `SetInterceptor` configuration. An interceptor is run before all commands are executed. This is typically used for configuring logging or other infrastructure concerns. diff --git a/src/Spectre.Console.Testing/Cli/TypeRegistrarBaseTests.cs b/src/Spectre.Console.Testing/Cli/TypeRegistrarBaseTests.cs new file mode 100644 index 0000000..8980a98 --- /dev/null +++ b/src/Spectre.Console.Testing/Cli/TypeRegistrarBaseTests.cs @@ -0,0 +1,191 @@ +using System; +using Spectre.Console.Cli; + +namespace Spectre.Console.Testing +{ + /// + /// This is a utility class for implementors of + /// and corresponding . + /// + public sealed class TypeRegistrarBaseTests + { + private readonly Func _registrarFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The factory to create a new, clean to be used for each test. + public TypeRegistrarBaseTests(Func registrarFactory) + { + _registrarFactory = registrarFactory; + } + + /// + /// Runs all tests. + /// + /// This exception is raised, if a test fails. + public void RunAllTests() + { + var testCases = new Action[] + { + RegistrationsCanBeResolved, + InstanceRegistrationsCanBeResolved, + LazyRegistrationsCanBeResolved, + ResolvingNotRegisteredServiceReturnsNull, + ResolvingNullTypeReturnsNull, + }; + + foreach (var test in testCases) + { + test(_registrarFactory()); + } + } + + private static void ResolvingNullTypeReturnsNull(ITypeRegistrar registrar) + { + // Given no registration + var resolver = registrar.Build(); + + try + { + // When + var actual = resolver.Resolve(null); + + // Then + if (actual != null) + { + throw new TestFailedException( + $"Expected the resolver to resolve null, since null was requested as the service type. Actually resolved {actual.GetType().Name}."); + } + } + catch (Exception ex) + { + throw new TestFailedException( + $"Expected the resolver not to throw, but caught {ex.GetType().Name}.", ex); + } + } + + private static void ResolvingNotRegisteredServiceReturnsNull(ITypeRegistrar registrar) + { + // Given no registration + var resolver = registrar.Build(); + + try + { + // When + var actual = resolver.Resolve(typeof(IMockService)); + + // Then + if (actual != null) + { + throw new TestFailedException( + $"Expected the resolver to resolve null, since no service was registered. Actually resolved {actual.GetType().Name}."); + } + } + catch (Exception ex) + { + throw new TestFailedException( + $"Expected the resolver not to throw, but caught {ex.GetType().Name}.", ex); + } + } + + private static void RegistrationsCanBeResolved(ITypeRegistrar registrar) + { + // Given + registrar.Register(typeof(IMockService), typeof(MockService)); + var resolver = registrar.Build(); + + // When + var actual = resolver.Resolve(typeof(IMockService)); + + // Then + if (actual == null) + { + throw new TestFailedException( + $"Expected the resolver to resolve an instance of {nameof(MockService)}. Actually resolved null."); + } + + if (actual is not MockService) + { + throw new TestFailedException( + $"Expected the resolver to resolve an instance of {nameof(MockService)}. Actually resolved {actual.GetType().Name}."); + } + } + + private static void InstanceRegistrationsCanBeResolved(ITypeRegistrar registrar) + { + // Given + var instance = new MockService(); + registrar.RegisterInstance(typeof(IMockService), instance); + var resolver = registrar.Build(); + + // When + var actual = resolver.Resolve(typeof(IMockService)); + + // Then + if (!ReferenceEquals(actual, instance)) + { + throw new TestFailedException( + "Expected the resolver to resolve exactly the registered instance."); + } + } + + private static void LazyRegistrationsCanBeResolved(ITypeRegistrar registrar) + { + // Given + var instance = new MockService(); + var factoryCalled = false; + registrar.RegisterLazy(typeof(IMockService), () => + { + factoryCalled = true; + return instance; + }); + var resolver = registrar.Build(); + + // When + var actual = resolver.Resolve(typeof(IMockService)); + + // Then + if (!factoryCalled) + { + throw new TestFailedException( + "Expected the factory to be called, to resolve the lazy registration."); + } + + if (!ReferenceEquals(actual, instance)) + { + throw new TestFailedException( + "Expected the resolver to return exactly the result of the lazy-registered factory."); + } + } + + /// + /// internal use only. + /// + private interface IMockService + { + } + + private class MockService : IMockService + { + } + + /// + /// Exception, to be raised when a test fails. + /// + public sealed class TestFailedException : Exception + { + /// + public TestFailedException(string message) + : base(message) + { + } + + /// + public TestFailedException(string message, Exception inner) + : base(message, inner) + { + } + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj index 41f0a16..2c90a3f 100644 --- a/src/Spectre.Console/Spectre.Console.csproj +++ b/src/Spectre.Console/Spectre.Console.csproj @@ -17,6 +17,7 @@ + diff --git a/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj b/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj index dd5bb05..d3e5802 100644 --- a/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj +++ b/test/Spectre.Console.Tests/Spectre.Console.Tests.csproj @@ -6,10 +6,6 @@ 9.0 - - - - @@ -24,7 +20,6 @@ - diff --git a/test/Spectre.Console.Tests/Unit/Cli/DefaultTypeRegistrarTests.cs b/test/Spectre.Console.Tests/Unit/Cli/DefaultTypeRegistrarTests.cs new file mode 100644 index 0000000..9356cfe --- /dev/null +++ b/test/Spectre.Console.Tests/Unit/Cli/DefaultTypeRegistrarTests.cs @@ -0,0 +1,16 @@ +using Spectre.Console.Cli; +using Spectre.Console.Testing; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed class DefaultTypeRegistrarTests + { + [Fact] + public void Should_Pass_Base_Registrar_Tests() + { + var harness = new TypeRegistrarBaseTests(() => new DefaultTypeRegistrar()); + harness.RunAllTests(); + } + } +} \ No newline at end of file