Skip to content

Commit

Permalink
Merge pull request #38 from messerli-informatik-ag/retrieve-types-fro…
Browse files Browse the repository at this point in the history
…m-container-attribute

Add attribute for retrieving types from a container as theory data
  • Loading branch information
dngroth authored Apr 6, 2020
2 parents 3a6b457 + f861dac commit ee8493e
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 5 deletions.
123 changes: 123 additions & 0 deletions Test.Utility.Test/TypesRegisteredInContainerDataRetrieverTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Autofac;
using Messerli.CompositionRoot;
using Xunit;

namespace Messerli.Test.Utility.Test
{
public class TypesRegisteredInContainerDataRetrieverTest
{
private interface IFoo
{
}

private interface IBar
{
}

[Fact]
public async Task RetrievesTypesRegisteredInContainer()
{
var expectedTypes = new[] { typeof(IFoo), typeof(IBar) };
var types = await GetTypesRegisteredInContainerViaMethod(nameof(CreateContainer));
Assert.Equal(expectedTypes, types);
}

[Fact]
public async Task WorksWithContainerThatNeedsToBeDisposedAsynchronously()
{
var expectedTypes = new[] { typeof(AsyncDisposable) };
var types = await GetTypesRegisteredInContainerViaMethod(nameof(CreateContainerWithAsyncDisposableObject));
Assert.Equal(expectedTypes, types);
}

[Fact]
public async Task ThrowsWhenCreateContainerMethodIsNotStatic()
{
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await GetTypesRegisteredInContainerViaMethod(nameof(CreateContainerNonStatic));
});
}

[Fact]
public async Task ThrowsWhenCreateContainerMethodIsPrivate()
{
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await GetTypesRegisteredInContainerViaMethod(nameof(CreateContainerPrivate));
});
}

[Fact]
public async Task ThrowsWhenCreateContainerHasIncorrectReturnType()
{
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await GetTypesRegisteredInContainerViaMethod(nameof(CreateContainerWithIncorrectReturnType));
});
}

[Fact]
public async Task ThrowsWhenCreateContainerHasParametersType()
{
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
{
await GetTypesRegisteredInContainerViaMethod(nameof(CreateContainerWithParameters));
});
}

public static IContainer CreateContainer()
=> new CompositionRootBuilder()
.RegisterModule<TestModule>()
.Build();

public static IContainer CreateContainerWithAsyncDisposableObject()
=> new CompositionRootBuilder()
.RegisterModule(new ModuleBuilder()
.RegisterInstance(new AsyncDisposable())
.Build())
.Build();

public static string CreateContainerWithIncorrectReturnType()
=> string.Empty;

public static IContainer CreateContainerWithParameters(string foo)
=> new CompositionRootBuilder().Build();

public IContainer CreateContainerNonStatic()
=> new CompositionRootBuilder().Build();

private static IContainer CreateContainerPrivate()
=> new CompositionRootBuilder().Build();

private static Task<IEnumerable<Type>> GetTypesRegisteredInContainerViaMethod(string createContainerMethodName)
=> TypesRegisteredInContainerRetriever.GetTypesRegisteredInContainerViaMethod(
typeof(TypesRegisteredInContainerDataRetrieverTest),
createContainerMethodName);

private class TestModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<Foo>().As<IFoo>();
builder.RegisterType<Bar>().As<IBar>();
}
}

private class Foo : IFoo
{
}

private class Bar : IBar
{
}

private class AsyncDisposable : IAsyncDisposable
{
public ValueTask DisposeAsync() => default;
}
}
}
4 changes: 2 additions & 2 deletions Test.Utility/ExcludedTypesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ namespace Messerli.Test.Utility
/// Blacklists a hardcoded list of types. Used together with <see cref="TypesThatNeedToBeImplementedInAssemblyDataAttribute"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class ExcludedTypesAttribute : Attribute
public sealed class ExcludedTypesAttribute : Attribute
{
public ExcludedTypesAttribute(params Type[] types)
{
Types = types;
}

public IEnumerable<Type> Types { get; }
internal IEnumerable<Type> Types { get; }
}
}
3 changes: 2 additions & 1 deletion Test.Utility/Test.Utility.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
<LangVersion>8.0</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<PackageId>Messerli.Test.Utility</PackageId>
<Version>0.7.3</Version>
<Version>0.8.0</Version>
<Description>Class library to simplify test-setups.</Description>
<RepositoryUrl>https://github.com/messerli-informatik-ag/test-utility</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand Down
70 changes: 70 additions & 0 deletions Test.Utility/TypesRegisteredInContainerDataAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Autofac;
using Xunit.Sdk;
using static Messerli.Test.Utility.TypesRegisteredInContainerRetriever;

namespace Messerli.Test.Utility
{
/// <summary>
/// <para>
/// Provides all types registered in an Autofac <see cref="IContainer"/> as theory data.
/// </para>
/// </summary>
/// <example>
/// <code>
/// using System;
/// using Autofac;
/// using Messerli.CompositionRoot;
/// using Xunit;
///
/// public sealed class FooModuleTest : IDisposable
/// {
/// private readonly IContainer _container = CreateContainer();
///
/// public static IContainer CreateContainer() => new CompositionRootBuilder().Build();
///
/// [Theory]
/// [TypesRegisteredInContainerData(nameof(CreateContainer))]
/// public void TypesRegisteredInContainerCanBeResolved(Type type) => _container.Resolve(type);
///
/// public void Dispose() => _container.Dispose();
/// }
/// </code>
/// </example>
public sealed class TypesRegisteredInContainerDataAttribute : DataAttribute
{
private readonly string _createContainerMethodName;

public TypesRegisteredInContainerDataAttribute(string createContainerMethodName)
{
_createContainerMethodName = createContainerMethodName;
}

public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
try
{
return GetTypesRegisteredInContainer(testMethod).Result;
}
catch (AggregateException exception) when (exception.Flatten().InnerExceptions.Count == 1)
{
ExceptionDispatchInfo.Capture(exception.Flatten().InnerException).Throw();
throw null!;
}
}

private async Task<IEnumerable<object[]>> GetTypesRegisteredInContainer(MemberInfo testMethod)
{
var targetType = testMethod.DeclaringType ?? throw new NullReferenceException();
var types = await GetTypesRegisteredInContainerViaMethod(targetType, _createContainerMethodName);
return types.Select(type => new[] { type });
}
}
}
53 changes: 53 additions & 0 deletions Test.Utility/TypesRegisteredInContainerRetriever.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Autofac;

namespace Messerli.Test.Utility
{
internal static class TypesRegisteredInContainerRetriever
{
public static async Task<IEnumerable<Type>> GetTypesRegisteredInContainerViaMethod(IReflect targetType, string createContainerMethodName)
{
await using var container = CreateContainer(targetType, createContainerMethodName);
return container.GetRegisteredTypes();
}

private static IContainer CreateContainer(IReflect targetType, string createContainerMethodName)
{
var createContainerMethod = GetCreateContainerMethod(targetType, createContainerMethodName);
return (IContainer?)createContainerMethod.Invoke(null, null)
?? throw new NullReferenceException();
}

private static MethodInfo GetCreateContainerMethod(IReflect type, string createContainerMethodName)
{
var createContainerMethod = type.GetMethod(createContainerMethodName, BindingFlags.Static | BindingFlags.Public)
?? throw new InvalidOperationException("Unable to access container creation method. Make sure it's static and public.");
ValidateCreateContainerParameters(createContainerMethod);
ValidateCreateContainerReturnType(createContainerMethod);
return createContainerMethod;
}

private static void ValidateCreateContainerParameters(MethodInfo method)
{
if (method.GetParameters().Any())
{
throw new InvalidOperationException("The container creation method must be nullary.");
}
}

private static void ValidateCreateContainerReturnType(MethodInfo method)
{
var expectedReturnType = typeof(IContainer);
var returnType = method.ReturnType;
if (!expectedReturnType.IsAssignableFrom(returnType))
{
throw new InvalidOperationException(
$"The return type of the container creation method should be '{expectedReturnType.Name}' but was '{returnType}'");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ namespace Messerli.Test.Utility
/// }
/// </code>
/// </example>
public class TypesThatNeedToBeImplementedInAssemblyDataAttribute : DataAttribute
public sealed class TypesThatNeedToBeImplementedInAssemblyDataAttribute : DataAttribute
{
private readonly string _assemblyName;

Expand Down
6 changes: 5 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ Rerelease of 0.6.0 with fixed nuget package.
## 0.7.3
- Allow blacklisting of types on `TypesThatNeedToBeImplementedInAssemblyData` attribute using a new attribute: `ExcludedTypes`.

## Unreleased
## 0.8.0
- Make sure that the exception thrown by the `TypesThatNeedToBeImplementedInAssemblyData` attribute
when the specified assembly can't be found is displayed in the test explorer.
- Add `TypesRegisteredInContainerDataAttribute` attribute which provides the types
registered in an Autofac container as xUnit theory data.
- Seal `ExcludedTypesAttribute` and make its `Types` member internal.
- Seal `TypesThatNeedToBeImplementedInAssemblyDataAttribute`.

0 comments on commit ee8493e

Please sign in to comment.