Skip to content

Commit

Permalink
(#555) added TypeRegistrarBaseTests
Browse files Browse the repository at this point in the history
as a very simple test harness to test implementations of
ITypeRegistrar / ITypeResolver
  • Loading branch information
nils-a committed Nov 13, 2021
1 parent fd181e2 commit 24e351d
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/input/cli/commandApp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
191 changes: 191 additions & 0 deletions src/Spectre.Console.Testing/Cli/TypeRegistrarBaseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using System;
using Spectre.Console.Cli;

namespace Spectre.Console.Testing
{
/// <summary>
/// This is a utility class for implementors of
/// <see cref="ITypeRegistrar"/> and corresponding <see cref="ITypeResolver"/>.
/// </summary>
public sealed class TypeRegistrarBaseTests
{
private readonly Func<ITypeRegistrar> _registrarFactory;

/// <summary>
/// Initializes a new instance of the <see cref="TypeRegistrarBaseTests"/> class.
/// </summary>
/// <param name="registrarFactory">The factory to create a new, clean <see cref="ITypeRegistrar"/> to be used for each test.</param>
public TypeRegistrarBaseTests(Func<ITypeRegistrar> registrarFactory)
{
_registrarFactory = registrarFactory;
}

/// <summary>
/// Runs all tests.
/// </summary>
/// <exception cref="TestFailedException">This exception is raised, if a test fails.</exception>
public void RunAllTests()
{
var testCases = new Action<ITypeRegistrar>[]
{
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.");
}
}

/// <summary>
/// internal use only.
/// </summary>
private interface IMockService
{
}

private class MockService : IMockService
{
}

/// <summary>
/// Exception, to be raised when a test fails.
/// </summary>
public sealed class TestFailedException : Exception
{
/// <inheritdoc cref="Exception" />
public TestFailedException(string message)
: base(message)
{
}

/// <inheritdoc cref="Exception" />
public TestFailedException(string message, Exception inner)
: base(message, inner)
{
}
}
}
}
3 changes: 3 additions & 0 deletions src/Spectre.Console/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Spectre.Console.Tests")]
16 changes: 16 additions & 0 deletions test/Spectre.Console.Tests/Unit/Cli/DefaultTypeRegistrarTests.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}

0 comments on commit 24e351d

Please sign in to comment.