diff --git a/CHANGELOG.md b/CHANGELOG.md index 5668189e5..f7383e08b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # [vNext] ## Improvements: +* Microsoft.Extensions.DependencyInjection.ReqnrollPlugin: Improved message when [ScenarioDependencies] can't be found or has an incorrect return type (#494) ## Bug fixes: * Fix: Microsoft.Extensions.DependencyInjection.ReqnrollPlugin, the plugin was only searching for [ScenarioDependencies] in assemblies with step definitions (#477) diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/InvalidScenarioDependenciesException.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/InvalidScenarioDependenciesException.cs new file mode 100644 index 000000000..b41f4ff36 --- /dev/null +++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/InvalidScenarioDependenciesException.cs @@ -0,0 +1,9 @@ +using System; + +namespace Reqnroll.Microsoft.Extensions.DependencyInjection; + +[Serializable] +public class InvalidScenarioDependenciesException(string reason) : ReqnrollException("[ScenarioDependencies] should return IServiceCollection but " + reason) +{ + public override string HelpLink { get; set; } = "https://go.reqnroll.net/doc-msdi"; +} \ No newline at end of file diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/MissingScenarioDependenciesException.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/MissingScenarioDependenciesException.cs index 581f75b0e..3984768c3 100644 --- a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/MissingScenarioDependenciesException.cs +++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/MissingScenarioDependenciesException.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Reqnroll.Microsoft.Extensions.DependencyInjection { @@ -6,9 +7,26 @@ namespace Reqnroll.Microsoft.Extensions.DependencyInjection public class MissingScenarioDependenciesException : ReqnrollException { public MissingScenarioDependenciesException() - : base("No method marked with [ScenarioDependencies] attribute found.") + : this([]) { - HelpLink = "https://go.reqnroll.net/doc-msdi"; } + + public MissingScenarioDependenciesException(IList assemblyNames) + : base(CreateMessage(assemblyNames)) + { + + } + + private static string CreateMessage(IList assemblyNames) + { + var message = "No method marked with [ScenarioDependencies] attribute found. It should be a (public or non-public) static method."; + if (assemblyNames.Count > 0) + { + message += $" Scanned assemblies: {string.Join(", ", assemblyNames)}."; + } + return message; + } + + public override string HelpLink { get; set; } = "https://go.reqnroll.net/doc-msdi"; } } diff --git a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs index 01ba64535..5ed766791 100644 --- a/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs +++ b/Plugins/Reqnroll.Microsoft.Extensions.DependencyInjection.ReqnrollPlugin/ServiceCollectionFinder.cs @@ -28,7 +28,7 @@ public ServiceCollectionFinder(ITestRunnerManager testRunnerManager) { foreach (var type in assembly.GetTypes()) { - foreach (var methodInfo in type.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) + foreach (MethodInfo methodInfo in type.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) { var scenarioDependenciesAttribute = (ScenarioDependenciesAttribute)Attribute.GetCustomAttribute(methodInfo, typeof(ScenarioDependenciesAttribute)); @@ -44,12 +44,28 @@ public ServiceCollectionFinder(ITestRunnerManager testRunnerManager) } } } - throw new MissingScenarioDependenciesException(); + var assemblyNames = assemblies.Select(a => a.GetName().Name).ToList(); + throw new MissingScenarioDependenciesException(assemblyNames); } - private static IServiceCollection GetServiceCollection(MethodBase methodInfo) + private static IServiceCollection GetServiceCollection(MethodInfo methodInfo) { - return (IServiceCollection)methodInfo.Invoke(null, null); + var serviceCollection = methodInfo.Invoke(null, null); + if(methodInfo.ReturnType == typeof(void)) + { + throw new InvalidScenarioDependenciesException("the method doesn't return a value."); + } + + if (serviceCollection == null) + { + throw new InvalidScenarioDependenciesException("returned null."); + } + + if (serviceCollection is not IServiceCollection collection) + { + throw new InvalidScenarioDependenciesException($"returned {serviceCollection.GetType()}."); + } + return collection; } private static void AddBindingAttributes(IEnumerable bindingAssemblies, IServiceCollection serviceCollection) diff --git a/Tests/Reqnroll.PluginTests/Microsoft.Extensions.DependencyInjection/ServiceCollectionFinderTests.cs b/Tests/Reqnroll.PluginTests/Microsoft.Extensions.DependencyInjection/ServiceCollectionFinderTests.cs index 577e86533..6a70828b0 100644 --- a/Tests/Reqnroll.PluginTests/Microsoft.Extensions.DependencyInjection/ServiceCollectionFinderTests.cs +++ b/Tests/Reqnroll.PluginTests/Microsoft.Extensions.DependencyInjection/ServiceCollectionFinderTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using FluentAssertions; @@ -27,7 +28,68 @@ public void GetServiceCollection_HappyPath_ResolvesCorrectServiceCollection() var testService = serviceProvider.GetRequiredService(); testService.Name.Should().Be("ValidStartWithAutoRegister"); } - + + [Fact] + public void GetServiceCollection_MethodIsVoid_ThrowsInvalidScenarioDependenciesException() + { + // Arrange + var testRunnerManagerMock = CreateTestRunnerManagerMock(typeof(InvalidStartVoid)); + var sut = new ServiceCollectionFinder(testRunnerManagerMock.Object); + + // Act + var act = () => sut.GetServiceCollection(); + + // Assert + act.Should().Throw() + .WithMessage("[ScenarioDependencies] should return IServiceCollection but the method doesn't return a value."); + } + + [Fact] + public void GetServiceCollection_MethodReturnsNull_ThrowsInvalidScenarioDependenciesException() + { + // Arrange + var testRunnerManagerMock = CreateTestRunnerManagerMock(typeof(InvalidStartNull)); + var sut = new ServiceCollectionFinder(testRunnerManagerMock.Object); + + // Act + var act = () => sut.GetServiceCollection(); + + // Assert + act.Should().Throw() + .WithMessage("[ScenarioDependencies] should return IServiceCollection but returned null."); + } + + + [Fact] + public void GetServiceCollection_MethodReturnsInvalidType_ThrowsInvalidScenarioDependenciesException() + { + // Arrange + var testRunnerManagerMock = CreateTestRunnerManagerMock(typeof(InvalidStartWrongType)); + var sut = new ServiceCollectionFinder(testRunnerManagerMock.Object); + + // Act + var act = () => sut.GetServiceCollection(); + + // Assert + act.Should().Throw() + .WithMessage("[ScenarioDependencies] should return IServiceCollection but returned System.Collections.Generic.List`1[System.String]."); + } + + [Fact] + public void GetServiceCollection_NotFound_ThrowsMissingScenarioDependenciesException() + { + // Arrange + var testRunnerManagerMock = CreateTestRunnerManagerMock(typeof(ServiceCollectionFinderTests)); + var sut = new ServiceCollectionFinder(testRunnerManagerMock.Object); + + // Act + var act = () => sut.GetServiceCollection(); + + // Assert + act.Should().Throw() + .WithMessage("No method marked with [ScenarioDependencies] attribute found. It should be a (public or non-public) static method. Scanned assemblies: Reqnroll.PluginTests."); + } + [Fact] public void GetServiceCollection_AutoRegisterBindingsTrue_RegisterBindingsAsScoped() { @@ -62,6 +124,11 @@ private static Mock CreateTestRunnerManagerMock(params Type[ { var assemblyMock = new Mock(); assemblyMock.Setup(m => m.GetTypes()).Returns(types.ToArray()); + if (types.Length > 0) + { + var assembly = types[0].Assembly; + assemblyMock.Setup(m => m.GetName()).Returns(assembly.GetName()); + } var testRunnerManagerMock = new Mock(); testRunnerManagerMock.Setup(m => m.BindingAssemblies).Returns([assemblyMock.Object]); @@ -94,6 +161,33 @@ public static IServiceCollection GetServices() return serviceCollection; } } + private class InvalidStartVoid + { + [ScenarioDependencies] + public static void GetServices() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(new TestInterface("ValidStartWithAutoRegister")); + } + } + + private class InvalidStartNull + { + [ScenarioDependencies] + public static IServiceCollection GetServices() + { + return null; + } + } + + private class InvalidStartWrongType + { + [ScenarioDependencies] + public static object GetServices() + { + return new List(); + } + } [Binding] private class Binding1; }