Added unit testing

This commit is contained in:
Samuele Lorefice
2026-06-15 00:37:40 +02:00
parent 8fc2fbc7f1
commit 5dfee34bf5
10 changed files with 569 additions and 2 deletions

View File

@@ -0,0 +1,127 @@
namespace Syrette.Tests;
public class ServiceContainerConstructorSelectionTests
{
public class CtorWithOptional
{
public ITestService? A { get; }
public string? Name { get; }
public CtorWithOptional(ITestService a, string? name = "default")
{
A = a;
Name = name;
}
}
public class CtorWithExactMatch
{
public int Value { get; }
public CtorWithExactMatch(int value) { Value = value; }
public CtorWithExactMatch(int value, string label) { Value = value; }
}
public class NoSatisfiableCtor
{
public NoSatisfiableCtor(int value) { }
}
[Fact]
public void Greedy_picks_most_satisfiable_constructor()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
container.AddSingleton<ITestServiceB, TestServiceBImpl>();
container.AddSingleton<TestMultiCtor>();
var instance = container.GetService<TestMultiCtor>();
Assert.NotNull(instance.A);
Assert.NotNull(instance.B);
Assert.Null(instance.C);
}
[Fact]
public void Greedy_picks_all_satisfied_over_partial()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
container.AddSingleton<ITestServiceB, TestServiceBImpl>();
container.AddSingleton<ITestServiceC, TestServiceCImpl>();
container.AddSingleton<TestMultiCtor>();
var instance = container.GetService<TestMultiCtor>();
Assert.NotNull(instance.A);
Assert.NotNull(instance.B);
Assert.NotNull(instance.C);
}
[Fact]
public void Greedy_falls_back_to_1_param_when_only_1_satisfiable()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
container.AddSingleton<TestMultiCtor>();
var instance = container.GetService<TestMultiCtor>();
Assert.NotNull(instance.A);
Assert.Null(instance.B);
Assert.Null(instance.C);
}
[Fact]
public void Optional_parameter_used_when_not_registered()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
container.AddSingleton<CtorWithOptional>();
var instance = container.GetService<CtorWithOptional>();
Assert.NotNull(instance.A);
Assert.Equal("default", instance.Name);
}
[Fact]
public void Registration_args_satisfy_constructor()
{
var container = new ServiceContainer();
container.AddSingleton<CtorWithExactMatch, CtorWithExactMatch>(42);
var instance = container.GetService<CtorWithExactMatch>();
Assert.Equal(42, instance.Value);
}
[Fact]
public void Resolution_args_satisfy_constructor()
{
var container = new ServiceContainer();
container.AddSingleton<CtorWithExactMatch, CtorWithExactMatch>();
var instance = (CtorWithExactMatch)container.GetService(
typeof(CtorWithExactMatch), new object[] { 99 });
Assert.Equal(99, instance.Value);
}
[Fact]
public void Resolution_args_override_registration_args()
{
var container = new ServiceContainer();
container.AddSingleton<CtorWithExactMatch, CtorWithExactMatch>(10);
var instance = (CtorWithExactMatch)container.GetService(
typeof(CtorWithExactMatch), new object[] { 20 });
Assert.Equal(20, instance.Value);
}
[Fact]
public void No_suitable_constructor_throws()
{
var container = new ServiceContainer();
container.AddSingleton<NoSatisfiableCtor>();
Assert.Throws<InvalidOperationException>(() =>
container.GetService<NoSatisfiableCtor>());
}
}

View File

@@ -0,0 +1,40 @@
namespace Syrette.Tests;
public class ServiceContainerDisposalTests
{
[Fact]
public void Dispose_disposes_singletons_implementing_IDisposable()
{
var container = new ServiceContainer();
container.AddSingleton<TestDisposableService>();
var instance = container.GetService<TestDisposableService>();
container.Dispose();
Assert.True(instance.IsDisposed);
}
[Fact]
public void Dispose_clears_singleton_cache()
{
var container = new ServiceContainer();
container.AddSingleton<TestDisposableService>();
container.GetService<TestDisposableService>();
container.Dispose();
Assert.Throws<InvalidOperationException>(() =>
container.GetService<TestDisposableService>());
}
[Fact]
public void Multiple_dispose_is_safe()
{
var container = new ServiceContainer();
container.AddSingleton<TestDisposableService>();
container.GetService<TestDisposableService>();
container.Dispose();
container.Dispose();
Assert.True(true);
}
}

View File

@@ -0,0 +1,100 @@
namespace Syrette.Tests;
public class ServiceContainerRegistrationTests
{
[Fact]
public void AddSingleton_TInterface_TImplementation_registers_successfully()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
Assert.NotNull(container.GetService<ITestService>());
}
[Fact]
public void AddSingleton_TInterface_TImplementation_with_args_registers_successfully()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestDeepService>(Guid.NewGuid());
Assert.NotNull(container.GetService<ITestService>());
}
[Fact]
public void AddSingleton_TClass_registers_successfully()
{
var container = new ServiceContainer();
container.AddSingleton<TestServiceImpl>();
Assert.NotNull(container.GetService<TestServiceImpl>());
}
[Fact]
public void AddSingleton_TClass_with_args_registers_successfully()
{
var container = new ServiceContainer();
container.AddSingleton<TestDeepService>(Guid.NewGuid());
Assert.NotNull(container.GetService<TestDeepService>());
}
[Fact]
public void AddTransient_TInterface_TImplementation_registers_successfully()
{
var container = new ServiceContainer();
container.AddTransient<ITestService, TestServiceImpl>();
Assert.NotNull(container.GetService<ITestService>());
}
[Fact]
public void AddTransient_TInterface_TImplementation_with_args_registers_successfully()
{
var container = new ServiceContainer();
container.AddTransient<ITestService, TestDeepService>(Guid.NewGuid());
Assert.NotNull(container.GetService<ITestService>());
}
[Fact]
public void AddTransient_TClass_registers_successfully()
{
var container = new ServiceContainer();
container.AddTransient<TestServiceImpl>();
Assert.NotNull(container.GetService<TestServiceImpl>());
}
[Fact]
public void AddTransient_TClass_with_args_registers_successfully()
{
var container = new ServiceContainer();
container.AddTransient<TestDeepService>(Guid.NewGuid());
Assert.NotNull(container.GetService<TestDeepService>());
}
[Fact]
public void Fluent_chaining_returns_same_container()
{
var container = new ServiceContainer();
var result = container
.AddSingleton<ITestService, TestServiceImpl>()
.AddTransient<TestServiceImpl>();
Assert.Same(container, result);
}
[Fact]
public void Duplicate_registration_same_pair_throws()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
Assert.Throws<InvalidOperationException>(() =>
container.AddSingleton<ITestService, TestServiceImpl>());
}
[Fact]
public void Duplicate_registration_different_impl_for_same_service_allowed()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
container.AddSingleton<ITestService, TestServiceAlt>();
var services = container.GetServiceTypes<ITestService>();
Assert.Equal(2, services.Count);
Assert.Contains(typeof(TestServiceImpl), services);
Assert.Contains(typeof(TestServiceAlt), services);
}
}

View File

@@ -0,0 +1,141 @@
namespace Syrette.Tests;
public class ServiceContainerResolutionTests
{
[Fact]
public void GetService_T_resolves_singleton()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
var instance = container.GetService<ITestService>();
Assert.NotNull(instance);
Assert.IsType<TestServiceImpl>(instance);
}
[Fact]
public void GetService_T_resolves_transient()
{
var container = new ServiceContainer();
container.AddTransient<ITestServiceB, TestServiceBImpl>();
var instance = container.GetService<ITestServiceB>();
Assert.NotNull(instance);
Assert.IsType<TestServiceBImpl>(instance);
}
[Fact]
public void Singleton_returns_same_instance()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
var a = container.GetService<ITestService>();
var b = container.GetService<ITestService>();
Assert.Same(a, b);
}
[Fact]
public void Transient_returns_new_instance()
{
var container = new ServiceContainer();
container.AddTransient<ITestServiceB, TestServiceBImpl>();
var a = container.GetService<ITestServiceB>();
var b = container.GetService<ITestServiceB>();
Assert.NotSame(a, b);
}
[Fact]
public void GetService_Type_non_generic_resolves()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
var instance = container.GetService(typeof(ITestService));
Assert.NotNull(instance);
Assert.IsType<TestServiceImpl>(instance);
}
[Fact]
public void GetService_Type_with_args_resolves()
{
var container = new ServiceContainer();
container.AddTransient<ITestService, TestDeepService>();
var instance = container.GetService(typeof(ITestService), new object[] { Guid.NewGuid() });
Assert.NotNull(instance);
}
[Fact]
public void GetService_T_throws_for_unregistered()
{
var container = new ServiceContainer();
Assert.Throws<InvalidOperationException>(() =>
container.GetService<ITestService>());
}
[Fact]
public void GetService_Type_throws_for_unregistered()
{
var container = new ServiceContainer();
Assert.Throws<InvalidOperationException>(() =>
container.GetService(typeof(ITestService)));
}
[Fact]
public void TryGetService_returns_null_for_unregistered()
{
var container = new ServiceContainer();
var result = container.TryGetService(typeof(ITestService));
Assert.Null(result);
}
[Fact]
public void TryGetService_returns_instance_when_registered()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
var result = container.TryGetService(typeof(ITestService));
Assert.NotNull(result);
}
[Fact]
public void Resolve_by_implementation_type_when_not_registered_directly()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
var instance = container.GetService(typeof(TestServiceImpl));
Assert.NotNull(instance);
}
[Fact]
public void Resolution_time_args_override_registration_time_args()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestDeepService>(Guid.Empty);
var resolved = container.GetService(typeof(ITestService), new object[] { Guid.NewGuid() });
Assert.NotNull(resolved);
var deep = (TestDeepService)resolved;
Assert.NotEqual(Guid.Empty, deep.Id);
}
[Fact]
public void GetServices_returns_all_registered_implementations()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
container.AddSingleton<ITestService, TestServiceAlt>();
var services = container.GetServices<ITestService>();
Assert.Equal(2, services.Count);
var types = services.Select(s => s.GetType()).ToList();
Assert.Contains(typeof(TestServiceImpl), types);
Assert.Contains(typeof(TestServiceAlt), types);
}
[Fact]
public void GetServiceTypes_returns_all_implementation_types()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
container.AddSingleton<ITestService, TestServiceAlt>();
var types = container.GetServiceTypes<ITestService>();
Assert.Equal(2, types.Count);
Assert.Contains(typeof(TestServiceImpl), types);
Assert.Contains(typeof(TestServiceAlt), types);
}
}

View File

@@ -0,0 +1,61 @@
namespace Syrette.Tests;
public class ServiceContainerThreadingTests
{
[Fact]
public void Concurrent_singleton_resolution_returns_same_instance()
{
var container = new ServiceContainer();
container.AddSingleton<ITestService, TestServiceImpl>();
var results = new ITestService[10];
Parallel.For(0, 10, i =>
{
results[i] = container.GetService<ITestService>();
});
for (int i = 1; i < results.Length; i++)
{
Assert.Same(results[0], results[i]);
}
}
[Fact]
public void Concurrent_transient_resolution_returns_unique_instances()
{
var container = new ServiceContainer();
container.AddTransient<ITestService, TestServiceImpl>();
var results = new ITestService[10];
Parallel.For(0, 10, i =>
{
results[i] = container.GetService<ITestService>();
});
for (int i = 0; i < results.Length; i++)
{
for (int j = i + 1; j < results.Length; j++)
{
Assert.NotSame(results[i], results[j]);
}
}
}
[Fact]
public void Concurrent_registration_and_resolution_does_not_crash()
{
var container = new ServiceContainer();
Parallel.Invoke(
() => { try { container.AddSingleton<ITestService, TestServiceImpl>(); } catch { } },
() => { try { container.AddSingleton<ITestService, TestServiceImpl>(); } catch { } },
() => { try { container.GetService<ITestService>(); } catch { } },
() => { try { container.AddSingleton<ITestService, TestServiceImpl>(); } catch { } },
() => { try { container.GetService<ITestService>(); } catch { } }
);
Assert.NotNull(container);
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Syrette\Syrette.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,35 @@
namespace Syrette.Tests;
public interface ITestService { }
public class TestServiceImpl : ITestService { }
public class TestServiceAlt : ITestService { }
public interface ITestServiceB { }
public class TestServiceBImpl : ITestServiceB { }
public interface ITestServiceC { }
public class TestServiceCImpl : ITestServiceC { }
public class TestMultiCtor
{
public ITestService? A { get; }
public ITestServiceB? B { get; }
public ITestServiceC? C { get; }
public TestMultiCtor(ITestService a) { A = a; }
public TestMultiCtor(ITestService a, ITestServiceB b) { A = a; B = b; }
public TestMultiCtor(ITestService a, ITestServiceB b, ITestServiceC c) { A = a; B = b; C = c; }
}
public class TestDeepService : ITestService
{
public Guid Id { get; }
public TestDeepService(Guid id) { Id = id; }
}
public class TestDisposableService : IDisposable
{
public bool IsDisposed { get; private set; }
public void Dispose() => IsDisposed = true;
}

View File

@@ -0,0 +1,10 @@
namespace Syrette.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@@ -2,16 +2,44 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syrette", "Syrette\Syrette.csproj", "{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syrette.Tests", "Syrette.Tests\Syrette.Tests.csproj", "{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x64.ActiveCfg = Debug|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x64.Build.0 = Debug|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x86.ActiveCfg = Debug|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Debug|x86.Build.0 = Debug|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|Any CPU.Build.0 = Release|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x64.ActiveCfg = Release|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x64.Build.0 = Release|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x86.ActiveCfg = Release|Any CPU
{4730ABA2-3979-4B74-A3FF-042F6C3C47D6}.Release|x86.Build.0 = Release|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x64.ActiveCfg = Debug|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x64.Build.0 = Debug|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x86.ActiveCfg = Debug|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Debug|x86.Build.0 = Debug|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|Any CPU.Build.0 = Release|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x64.ActiveCfg = Release|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x64.Build.0 = Release|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x86.ActiveCfg = Release|Any CPU
{CE07A0AF-1EAB-4AFF-B04F-16E0A8A5FC24}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -165,12 +165,12 @@ public class ServiceContainer : IDisposable {
if (descriptor.Lifetime == ServiceLifetime.Singleton) {
lock (singletonLock) {
if (singletons.TryGetValue(descriptor.ServiceType, out var singleton)) {
if (singletons.TryGetValue(descriptor.ImplementationType, out var singleton)) {
return singleton;
}
var instance = Instantiate(descriptor.ImplementationType, bestCtor, mergedArgs);
singletons[descriptor.ServiceType] = instance;
singletons[descriptor.ImplementationType] = instance;
return instance;
}
}