Skip to content

Instantly share code, notes, and snippets.

@jacob7395
Last active November 19, 2024 06:20
Show Gist options
  • Save jacob7395/cd0095c825e8ce229d5265df1527eac9 to your computer and use it in GitHub Desktop.
Save jacob7395/cd0095c825e8ce229d5265df1527eac9 to your computer and use it in GitHub Desktop.
TUnit Dependency Injected Properties
public class DependencyInjectionClassConstructor : DiClassConstructorBase
{
public override T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
ClassConstructorMetadata classConstructorMetadata) where T : class
{
IServiceProvider provider = CreateServiceProvider<T>();
return ActivatorUtilities.GetServiceOrCreateInstance<T>(provider);
}
}
public abstract class DiClassConstructorBase : IClassConstructor, ILastTestInClassEventReceiver, ITestStartEventReceiver,
ILastTestInTestSessionEventReceiver
{
private static readonly IMongoRunner Runner;
private static readonly ConcurrentDictionary<Type, ConcurrentBag<ServiceProvider>> ServiceProviders;
public abstract T Create<T>(ClassConstructorMetadata classConstructorMetadata) where T : class;
static DiClassConstructorBase()
{
Runner = MongoRunner.Run();
ServiceProviders = new();
}
protected IEnumerable<Type> GetSubstitutedTypes<T>()
=> typeof(T).GetConstructors(BindingFlags.Public | BindingFlags.Instance)
.SelectMany(m => m.GetParameters())
.Where(p => p.GetCustomAttribute<SubstituteInstanceAttribute>() is not null)
.Select(p => p.ParameterType)
.Distinct();
public virtual async ValueTask OnTestStart(BeforeTestContext beforeTestContext)
{
if (ServiceProviders.TryGetValue(beforeTestContext.TestDetails.ClassType, out var scope))
{
foreach (IMigrationsContext context in scope.SelectMany(s => s.GetServices<IMigrationsContext>()))
{
await context.ApplyMigrationsAsync();
}
}
}
public virtual async ValueTask IfLastTestInClass(ClassHookContext context, TestContext testContext)
{
if (ServiceProviders.TryRemove(context.ClassType, out var serviceProviders))
{
foreach (ServiceProvider serviceProvider in serviceProviders)
{
await serviceProvider.DisposeAsync();
}
}
}
protected void AddAndRemoveType(IServiceCollection serviceCollection, Type interfaceType, object instance)
{
RemoveType(serviceCollection, interfaceType);
serviceCollection.AddSingleton(interfaceType, instance);
}
private static void RemoveType(IServiceCollection serviceCollection, Type interfaceType)
{
foreach (var serviceDescriptor in serviceCollection.Where(s => s.ServiceType == interfaceType).ToList())
{
serviceCollection.Remove(serviceDescriptor);
}
}
protected ServiceProvider CreateServiceProvider<T>()
{
var serviceProvider = UserHubConfigurationHelper.BasicProvider(
collection =>
{
collection.AddCostServiceMediatR();
ConfigureTestServices(collection);
AddAndRemoveType(collection, typeof(IMessageBus), Substitute.For<IMessageBus>());
foreach (Type subType in GetSubstitutedTypes<T>())
{
AddAndRemoveType(collection, subType, Substitute.For([subType], []));
}
RemoveType(collection, typeof(IMongoRunner));
collection.AddSingleton<IMongoRunner>(_ => new EphemeralMongoRunnerTestWrapper(Runner));
});
ServiceProviders.AddOrUpdate(
typeof(T),
new ConcurrentBag<ServiceProvider>([serviceProvider]),
(_, bag) =>
{
bag.Add(serviceProvider);
return bag;
});
return serviceProvider;
}
protected virtual IServiceCollection ConfigureTestServices(IServiceCollection serviceCollection)
{
return serviceCollection;
}
// this is to try and clean up my local monog that gets spun up for tests, there are some issues with the process sticking around
public virtual async ValueTask IfLastTestInTestSession(TestSessionContext current, TestContext testContext)
{
try
{
string processName = "mongodb";
var startInfo = new ProcessStartInfo
{
FileName = "taskkill",
Arguments = $"/IM {processName}.exe /F",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using Process? process = Process.Start(startInfo);
if (process is not null)
{
await process.WaitForExitAsync();
}
}
catch (Exception e)
{
current.ErrorOutputWriter.WriteLine(e);
}
}
}
// Also have a version that is scoped the entire test class, but I don't use this anymore so might have some issues
public class ClassScopedDiClassConstructor : DiClassConstructorBase
{
protected static readonly Lock ClassLock = Lock.Monitor();
protected static readonly ConcurrentDictionary<Type, AsyncServiceScope> ClassScopedProviders = new();
public IServiceProvider GetClassScopedProvider<T>()
{
if (ClassScopedProviders.TryGetValue(typeof(T), out var scopes)) return scopes.ServiceProvider;
using (ClassLock.Acquire())
{
if (ClassScopedProviders.TryGetValue(typeof(T), out var newScopes)) return newScopes.ServiceProvider;
AsyncServiceScope serviceProvider = CreateServiceProvider<T>().CreateAsyncScope();
return ClassScopedProviders.GetOrAdd(typeof(T), serviceProvider).ServiceProvider;
}
}
public override T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(
ClassConstructorMetadata classConstructorMetadata) where T : class
{
var provider = GetClassScopedProvider<T>();
return ActivatorUtilities.GetServiceOrCreateInstance<T>(provider);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment