Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved message when [ScenarioDependencies] can't be found or has an incorrect return type (Reqnroll.Microsoft.Extensions.DependencyInjection) #494

Merged
merged 6 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
using System;
using System.Collections.Generic;

namespace Reqnroll.Microsoft.Extensions.DependencyInjection
{
[Serializable]
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<string> assemblyNames)
: base(CreateMessage(assemblyNames))
{

}

private static string CreateMessage(IList<string> 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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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<Assembly> bindingAssemblies, IServiceCollection serviceCollection)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FluentAssertions;
Expand Down Expand Up @@ -27,7 +28,68 @@ public void GetServiceCollection_HappyPath_ResolvesCorrectServiceCollection()
var testService = serviceProvider.GetRequiredService<ITestInterface>();
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<InvalidScenarioDependenciesException>()
.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<InvalidScenarioDependenciesException>()
.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<InvalidScenarioDependenciesException>()
.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<MissingScenarioDependenciesException>()
.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()
{
Expand Down Expand Up @@ -62,6 +124,11 @@ private static Mock<ITestRunnerManager> CreateTestRunnerManagerMock(params Type[
{
var assemblyMock = new Mock<Assembly>();
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<ITestRunnerManager>();
testRunnerManagerMock.Setup(m => m.BindingAssemblies).Returns([assemblyMock.Object]);
Expand Down Expand Up @@ -94,6 +161,33 @@ public static IServiceCollection GetServices()
return serviceCollection;
}
}
private class InvalidStartVoid
{
[ScenarioDependencies]
public static void GetServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<ITestInterface>(new TestInterface("ValidStartWithAutoRegister"));
}
}

private class InvalidStartNull
{
[ScenarioDependencies]
public static IServiceCollection GetServices()
{
return null;
}
}

private class InvalidStartWrongType
{
[ScenarioDependencies]
public static object GetServices()
{
return new List<string>();
}
}
[Binding]
private class Binding1;
}