From e25c6ac1ddad78aee31b0285ed5987a7c2a2df55 Mon Sep 17 00:00:00 2001 From: Rinke Date: Sat, 5 Oct 2024 16:23:00 +0200 Subject: [PATCH] Use generic object for keyed services (#169) Co-authored-by: Rinke Blootens Co-authored-by: Kyle Dodson --- .../Services/TestServiceProvider.cs | 129 ++++++++++-------- .../Services/TestServicesExtensions.cs | 35 ++++- test/OrleansTestKit.Tests/Grains/DIGrain.cs | 18 --- .../Grains/DependencyGrain.cs | 37 +++++ .../Interfaces/IDependencyGrain.cs | 10 ++ .../Tests/DIGrainTests.cs | 30 ---- .../Tests/DependencyGrainTests.cs | 60 ++++++++ 7 files changed, 212 insertions(+), 107 deletions(-) delete mode 100644 test/OrleansTestKit.Tests/Grains/DIGrain.cs create mode 100644 test/OrleansTestKit.Tests/Grains/DependencyGrain.cs create mode 100644 test/OrleansTestKit.Tests/Interfaces/IDependencyGrain.cs delete mode 100644 test/OrleansTestKit.Tests/Tests/DIGrainTests.cs create mode 100644 test/OrleansTestKit.Tests/Tests/DependencyGrainTests.cs diff --git a/src/OrleansTestKit/Services/TestServiceProvider.cs b/src/OrleansTestKit/Services/TestServiceProvider.cs index 8c98d0a..6f1fcf1 100644 --- a/src/OrleansTestKit/Services/TestServiceProvider.cs +++ b/src/OrleansTestKit/Services/TestServiceProvider.cs @@ -5,18 +5,16 @@ namespace Orleans.TestKit.Services; -/// -/// The test service provider -/// +/// The test service provider public sealed class TestServiceProvider : IServiceProvider, IKeyedServiceProvider { + private readonly Dictionary<(object?, Type), object> _keyedServices = new(); + private readonly TestKitOptions _options; + private readonly Dictionary _services = new(); - private readonly Dictionary<(object?, Type), object> _keyedServices = new(); - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// The test kit options to use public TestServiceProvider(TestKitOptions options) { @@ -24,87 +22,70 @@ public TestServiceProvider(TestKitOptions options) _options = options; } - /// - /// Adds or updates a service to the provider - /// + /// Adds or updates a keyed service to the provider /// The service type + /// The service key /// The instance to add /// The instance /// Instance must be not null - public T AddService(T instance) + public T AddKeyedService(object? serviceKey, T instance) { ArgumentNullException.ThrowIfNull(instance); - _services[typeof(T)] = instance; + _keyedServices[(serviceKey, typeof(T))] = instance; return instance; } - /// - /// Adds or updates a keyed service to the provider - /// + /// Adds or updates a keyed mock to the service provider + /// The underlying service type + /// The service key + /// The newly created mock + public Mock AddKeyedServiceProbe(object? serviceKey) + where T : class => + AddKeyedServiceProbe(serviceKey, new Mock()); + + /// Adds or updates a keyed mock to the service provider + /// The underlying service type + /// The service key + /// The mock to add + /// The mock + public Mock AddKeyedServiceProbe(object? serviceKey, Mock mock) + where T : class + { + AddKeyedService(serviceKey, mock.Object); + return mock; + } + + /// Adds or updates a service to the provider /// The service type - /// The service key /// The instance to add /// The instance /// Instance must be not null - public T AddKeyedService(string name, T instance) + public T AddService(T instance) { ArgumentNullException.ThrowIfNull(instance); - _keyedServices[(name, typeof(T))] = instance; + _services[typeof(T)] = instance; return instance; } - /// - /// Adds a mock to the service provider - /// + /// Adds or updates a mock to the service provider /// The underlying service type /// The mock to add /// The mock - public Mock AddServiceProbe(Mock mock) where T : class + public Mock AddServiceProbe(Mock mock) + where T : class { AddService(mock.Object); return mock; } - /// - /// Adds a mock to the service provider - /// + /// Adds a mock to the service provider /// The underlying service type /// The newly created mock - public Mock AddServiceProbe() where T : class => AddServiceProbe(new Mock()); - - /// - public object GetService(Type serviceType) - { - ArgumentNullException.ThrowIfNull(serviceType); - - if (_services.TryGetValue(serviceType, out var service)) - { - return service; - } - - // If using strict service probes, throw the exception - if (_options.StrictServiceProbes) - { - throw new Exception($"Service probe does not exist for type {serviceType.Name}. Ensure that it is added before the grain is tested."); - } - else - { - // Create a new mock - if (Activator.CreateInstance(typeof(Mock<>).MakeGenericType(serviceType)) is not IMock mock) - { - throw new Exception($"Failed to instantiate {serviceType.Name}."); - } - - service = mock.Object; - - // Save the newly created grain for the next call - _services.Add(serviceType, service); - - return service; - } - } + public Mock AddServiceProbe() + where T : class => + AddServiceProbe(new Mock()); public object? GetKeyedService(Type serviceType, object? serviceKey) { @@ -155,4 +136,36 @@ public object GetRequiredKeyedService(Type serviceType, object? serviceKey) return service; } + + /// + public object GetService(Type serviceType) + { + ArgumentNullException.ThrowIfNull(serviceType); + + if (_services.TryGetValue(serviceType, out var service)) + { + return service; + } + + // If using strict service probes, throw the exception + if (_options.StrictServiceProbes) + { + throw new Exception($"Service probe does not exist for type {serviceType.Name}. Ensure that it is added before the grain is tested."); + } + else + { + // Create a new mock + if (Activator.CreateInstance(typeof(Mock<>).MakeGenericType(serviceType)) is not IMock mock) + { + throw new Exception($"Failed to instantiate {serviceType.Name}."); + } + + service = mock.Object; + + // Save the newly created grain for the next call + _services.Add(serviceType, service); + + return service; + } + } } diff --git a/src/OrleansTestKit/Services/TestServicesExtensions.cs b/src/OrleansTestKit/Services/TestServicesExtensions.cs index 5ee5c58..0311b26 100644 --- a/src/OrleansTestKit/Services/TestServicesExtensions.cs +++ b/src/OrleansTestKit/Services/TestServicesExtensions.cs @@ -4,7 +4,29 @@ namespace Orleans.TestKit; public static class TestServicesExtensions { - public static T AddService(this TestKitSilo silo, T instance) + public static T AddKeyedService(this TestKitSilo silo, object? serviceKey, T instance) + where T : class + { + if (silo == null) + { + throw new ArgumentNullException(nameof(silo)); + } + + return silo.ServiceProvider.AddKeyedService(serviceKey, instance); + } + + public static Mock AddKeyedServiceProbe(this TestKitSilo silo, object? serviceKey) + where T : class + { + if (silo == null) + { + throw new ArgumentNullException(nameof(silo)); + } + + return silo.ServiceProvider.AddKeyedServiceProbe(serviceKey); + } + + public static Mock AddKeyedServiceProbe(this TestKitSilo silo, object? serviceKey, Mock mock) where T : class { if (silo == null) @@ -12,6 +34,17 @@ public static T AddService(this TestKitSilo silo, T instance) throw new ArgumentNullException(nameof(silo)); } + return silo.ServiceProvider.AddKeyedServiceProbe(serviceKey, mock); + } + + public static T AddService(this TestKitSilo silo, T instance) + where T : class + { + if (silo == null) + { + throw new ArgumentNullException(nameof(silo)); + } + return silo.ServiceProvider.AddService(instance); } diff --git a/test/OrleansTestKit.Tests/Grains/DIGrain.cs b/test/OrleansTestKit.Tests/Grains/DIGrain.cs deleted file mode 100644 index ae056fc..0000000 --- a/test/OrleansTestKit.Tests/Grains/DIGrain.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace TestGrains; - -public interface IDIService -{ - bool GetValue(); -} - -public sealed class DIGrain : Grain, IGrainWithGuidKey -{ - public DIGrain(IDIService service) - { - Service = service; - } - - public IDIService Service { get; } - - public bool GetServiceValue() => Service.GetValue(); -} diff --git a/test/OrleansTestKit.Tests/Grains/DependencyGrain.cs b/test/OrleansTestKit.Tests/Grains/DependencyGrain.cs new file mode 100644 index 0000000..dac5be2 --- /dev/null +++ b/test/OrleansTestKit.Tests/Grains/DependencyGrain.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.DependencyInjection; +using TestInterfaces; + +namespace TestGrains; + +public interface IDependency +{ + string? GetValue(); +} + +public sealed class DependencyGrain : Grain, IDependencyGrain +{ + private readonly IDependency? _firstKeyedDependency; + + private readonly IDependency? _secondKeyedDependency; + + private readonly IDependency? _unkeyedDependency; + + public DependencyGrain( + IDependency? unkeyedDependency, + [FromKeyedServices("first")] IDependency? firstKeyedDependency, + [FromKeyedServices("second")] IDependency? secondKeyedDependency) + { + _unkeyedDependency = unkeyedDependency; + _firstKeyedDependency = firstKeyedDependency; + _secondKeyedDependency = secondKeyedDependency; + } + + public Task GetFirstKeyedServiceValue() => + Task.FromResult(_firstKeyedDependency?.GetValue()); + + public Task GetSecondKeyedServiceValue() => + Task.FromResult(_secondKeyedDependency?.GetValue()); + + public Task GetUnkeyedServiceValue() => + Task.FromResult(_unkeyedDependency?.GetValue()); +} diff --git a/test/OrleansTestKit.Tests/Interfaces/IDependencyGrain.cs b/test/OrleansTestKit.Tests/Interfaces/IDependencyGrain.cs new file mode 100644 index 0000000..b777818 --- /dev/null +++ b/test/OrleansTestKit.Tests/Interfaces/IDependencyGrain.cs @@ -0,0 +1,10 @@ +namespace TestInterfaces; + +public interface IDependencyGrain : IGrainWithGuidKey +{ + Task GetFirstKeyedServiceValue(); + + Task GetSecondKeyedServiceValue(); + + Task GetUnkeyedServiceValue(); +} diff --git a/test/OrleansTestKit.Tests/Tests/DIGrainTests.cs b/test/OrleansTestKit.Tests/Tests/DIGrainTests.cs deleted file mode 100644 index 5f5cc38..0000000 --- a/test/OrleansTestKit.Tests/Tests/DIGrainTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentAssertions; -using Moq; -using TestGrains; -using Xunit; - -namespace Orleans.TestKit.Tests; - -public class DIGrainTests : TestKitBase -{ - [Fact] - public async Task CreateGrainWithService() - { - var grain = await Silo.CreateGrainAsync(Guid.NewGuid()); - - grain.Service.Should().NotBeNull(); - } - - [Fact] - public async Task SetupGrainService() - { - var mockSvc = new Mock(); - mockSvc.Setup(x => x.GetValue()).Returns(true); - - Silo.ServiceProvider.AddServiceProbe(mockSvc); - var grain = await Silo.CreateGrainAsync(Guid.NewGuid()); - - grain.GetServiceValue().Should().BeTrue(); - mockSvc.Verify(x => x.GetValue(), Times.Once); - } -} diff --git a/test/OrleansTestKit.Tests/Tests/DependencyGrainTests.cs b/test/OrleansTestKit.Tests/Tests/DependencyGrainTests.cs new file mode 100644 index 0000000..fd86810 --- /dev/null +++ b/test/OrleansTestKit.Tests/Tests/DependencyGrainTests.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using Moq; +using TestGrains; +using Xunit; + +namespace Orleans.TestKit.Tests; + +public class DependencyGrainTests : TestKitBase +{ + [Fact] + public async Task GrainWithoutServices() + { + // Arrange + var grain = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Act + var unkeyedServiceValue = await grain.GetUnkeyedServiceValue(); + var firstKeyedServiceValue = await grain.GetFirstKeyedServiceValue(); + var secondKeyedServiceValue = await grain.GetSecondKeyedServiceValue(); + + // Assert + unkeyedServiceValue.Should().BeNull(); + firstKeyedServiceValue.Should().BeNull(); + secondKeyedServiceValue.Should().BeNull(); + } + + [Fact] + public async Task GrainWithServices() + { + // Arrange + var unkeyedService = new Mock(MockBehavior.Strict); + unkeyedService.Setup(x => x.GetValue()).Returns(""); + Silo.ServiceProvider.AddServiceProbe(unkeyedService); + + var firstKeyedService = new Mock(MockBehavior.Strict); + firstKeyedService.Setup(x => x.GetValue()).Returns("first"); + Silo.ServiceProvider.AddKeyedServiceProbe("first", firstKeyedService); + + var secondKeyedService = new Mock(MockBehavior.Strict); + secondKeyedService.Setup(x => x.GetValue()).Returns("second"); + Silo.ServiceProvider.AddKeyedServiceProbe("second", secondKeyedService); + + var grain = await Silo.CreateGrainAsync(Guid.NewGuid()); + + // Act + var unkeyedServiceValue = await grain.GetUnkeyedServiceValue(); + var firstKeyedServiceValue = await grain.GetFirstKeyedServiceValue(); + var secondKeyedServiceValue = await grain.GetSecondKeyedServiceValue(); + + // Assert + unkeyedServiceValue.Should().BeEmpty(); + unkeyedService.Verify(x => x.GetValue(), Times.Once); + + firstKeyedServiceValue.Should().Be("first"); + firstKeyedService.Verify(x => x.GetValue(), Times.Once); + + secondKeyedServiceValue.Should().Be("second"); + secondKeyedService.Verify(x => x.GetValue(), Times.Once); + } +}