Skip to content
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
46 changes: 46 additions & 0 deletions integ-tests/SutProject.NUnitTests/EnvironmentSeedingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Reflection;
using DependencyModules.NUnit.Attributes;
using DependencyModules.Runtime;
using DependencyModules.Runtime.Attributes;
using DependencyModules.Runtime.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;

namespace SutProject.NUnitTests;

/// <summary>
/// The NUnit twin of the xUnit environment-seeding tests: the seeded environment reaches module
/// conditions through the real runner, which applies modules before the service-setup pass.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public class SeededEnvironmentAttribute(string name) : Attribute, IModuleEnvironmentProvider {
public IModuleEnvironment? ProvideEnvironment(MethodInfo testMethod) =>
new ModuleEnvironment(false, name);
}

public interface IGatedByEnvironment { }

[SingletonService(Realm = typeof(SeededEnvironmentModule))]
[IfEnvironment("seeded-environment")]
public class GatedByEnvironment : IGatedByEnvironment { }

[DependencyModule(OnlyRealm = true)]
public partial class SeededEnvironmentModule { }

public class EnvironmentSeedingTests {

[ModuleTest(typeof(SeededEnvironmentModule))]
[SeededEnvironment("seeded-environment")]
public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider provider) {
Assert.That(provider.GetService<IGatedByEnvironment>(), Is.Not.Null);
}

/// <summary>
/// The gate has to hold in the other direction, or the test above passes because the
/// condition was never compiled in.
/// </summary>
[ModuleTest(typeof(SeededEnvironmentModule))]
public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) {
Assert.That(provider.GetService<IGatedByEnvironment>(), Is.Null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System.Reflection;
using DependencyModules.Runtime;
using DependencyModules.Runtime.Attributes;
using DependencyModules.Runtime.Interfaces;
using DependencyModules.xUnit.Attributes;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace SutProject.Tests.EnvironmentTests;

/// <summary>
/// What an integration's environment attribute looks like: name the environment, hand it over.
/// Pinned away from process variables so a machine's ASPNETCORE_ENVIRONMENT cannot reach these
/// tests.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
public class SeededEnvironmentAttribute(string name) : Attribute, IModuleEnvironmentProvider {
public IModuleEnvironment? ProvideEnvironment(MethodInfo testMethod) =>
new ModuleEnvironment(false, name);
}

public interface IGatedByEnvironment { }

[SingletonService(Realm = typeof(SeededEnvironmentModule))]
[IfEnvironment("seeded-environment")]
public class GatedByEnvironment : IGatedByEnvironment { }

[DependencyModule(OnlyRealm = true)]
public partial class SeededEnvironmentModule { }

/// <summary>
/// The seeded environment reaches module conditions through the real test runner.
/// </summary>
/// <remarks>
/// <see cref="EnvironmentConfigurationTests"/> proves the same behaviour for a hand-built
/// collection through <c>AddModules</c>. These run through <c>[ModuleTest]</c> itself, which is
/// the path that had no way to supply an environment at all: modules are applied before the
/// service-setup pass, so every condition had been decided against the process default before an
/// attribute could register anything.
/// </remarks>
public class EnvironmentSeedingTests {

[ModuleTest]
[SeededEnvironmentModule]
[SeededEnvironment("seeded-environment")]
public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider provider) {
Assert.NotNull(provider.GetService<IGatedByEnvironment>());
}

/// <summary>
/// The gate has to hold in the other direction, or the test above passes because the
/// condition was never compiled in.
/// </summary>
[ModuleTest]
[SeededEnvironmentModule]
public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) {
Assert.Null(provider.GetService<IGatedByEnvironment>());
}

/// <summary>The environment a module reads is the seeded instance, not a parallel default.</summary>
[ModuleTest]
[EnvironmentAwareModule]
[SeededEnvironment("seeded-environment")]
public void AModuleReadingTheEnvironmentSeesTheSeededOne(IEnvironmentDependency dependency) {
Assert.Equal("seeded-environment", dependency.EnvironmentName);
}

[ModuleTest]
[EnvironmentAwareModule]
public void WithoutASeedTheProcessDefaultApplies(IEnvironmentDependency dependency) {
Assert.Equal(ModuleEnvironment.CreateDefault().EnvironmentName, dependency.EnvironmentName);
}
}

/// <summary>Narrowest scope wins, matching how every other attribute here resolves.</summary>
[SeededEnvironment("outer-environment")]
public class EnvironmentSeedingPrecedenceTests {

[ModuleTest]
[EnvironmentAwareModule]
public void AClassLevelSeedApplies(IEnvironmentDependency dependency) {
Assert.Equal("outer-environment", dependency.EnvironmentName);
}

[ModuleTest]
[EnvironmentAwareModule]
[SeededEnvironment("inner-environment")]
public void TheMethodsSeedBeatsTheClasses(IEnvironmentDependency dependency) {
Assert.Equal("inner-environment", dependency.EnvironmentName);
}
}
21 changes: 21 additions & 0 deletions src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public override TestResult Execute(TestExecutionContext context) {

SetupTestCaseInfo(serviceCollection, testMethod, knownAttributes);

SeedEnvironment(serviceCollection, method, knownAttributes);

SetupModules(serviceCollection, method, knownAttributes);

SetupServiceSetupAttributes(moduleContext, serviceCollection, knownAttributes);
Expand Down Expand Up @@ -143,6 +145,25 @@ private static void SetupServiceSetupAttributes(
}
}

/// <summary>
/// Registers the environment the test's attributes declare, ahead of the modules - the same
/// seeding the xUnit integration does, for the reason recorded there: conditions are answered
/// from the environment already in the collection as each module is applied, and the
/// service-setup pass runs too late to supply it.
/// </summary>
private static void SeedEnvironment(
IServiceCollection serviceCollection, MethodInfo method, Attribute[] knownAttributes) {
IModuleEnvironment? environment = null;

foreach (var provider in knownAttributes.OfType<IModuleEnvironmentProvider>()) {
environment = provider.ProvideEnvironment(method) ?? environment;
}

if (environment != null) {
serviceCollection.Add(new ServiceDescriptor(typeof(IModuleEnvironment), environment));
}
}

/// <remarks>
/// The same loading the xUnit integration does, reading <see cref="IModuleTestAttribute"/> so
/// neither names the other's attribute. It is not shared code because it needs both
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Reflection;

namespace DependencyModules.Runtime.Interfaces;

/// <summary>
/// An attribute that names the environment a test's container is built for, before any module
/// is applied.
/// </summary>
/// <remarks>
/// <para>
/// Module registrations are conditioned as they are applied: <c>[IfEnvironment]</c> and its
/// siblings are answered from the <see cref="IModuleEnvironment" /> already in the collection,
/// or from a process default when there is none. The test integrations run their service-setup
/// pass after the modules by design - a test registration beats an application one - so an
/// environment registered there arrives after every condition has been decided against the
/// default, and a test had no way to put itself under the environment it declares. The
/// integrations consult this before loading any module, which is the whole difference.
/// </para>
/// <para>
/// Beside <see cref="IDependencyModuleProvider" /> rather than in the Testing package, for the
/// same reason the module loading it feeds lives in each integration: it returns a Runtime
/// type, and Testing deliberately does not reference Runtime. The parameter is a
/// <see cref="MethodInfo" /> rather than a test-method context for the same reason.
/// </para>
/// <para>
/// Attributes are consulted widest scope first - assembly, then class, then method - and the
/// narrowest one that answers decides, matching how every other attribute resolves.
/// </para>
/// </remarks>
public interface IModuleEnvironmentProvider {

/// <summary>
/// The environment module conditions are evaluated against, or null to leave the decision
/// to a wider scope.
/// </summary>
/// <param name="testMethod">The test method the container is being built for.</param>
IModuleEnvironment? ProvideEnvironment(MethodInfo testMethod);
}
26 changes: 26 additions & 0 deletions src/DependencyModules.xUnit/Impl/ModuleTestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ private async Task<StartupValues> SetupServiceCollection() {

SetupTestCaseInfo(serviceCollection, knownAttributes);

SeedEnvironment(serviceCollection, knownAttributes);

SetupModules(serviceCollection, knownAttributes);

SetupServiceSetupAttributes(context, serviceCollection, knownAttributes);
Expand Down Expand Up @@ -160,6 +162,30 @@ private void SetupServiceSetupAttributes(
}
}

/// <summary>
/// Registers the environment the test's attributes declare, ahead of the modules.
/// </summary>
/// <remarks>
/// Before <see cref="SetupModules"/>, because module registrations are conditioned as they are
/// applied: <c>LoadModules</c> answers <c>[IfEnvironment]</c> from the
/// <see cref="IModuleEnvironment"/> already in the collection, or a process default when there
/// is none. The service-setup pass runs after the modules by design - a test registration
/// beats an application one - so an environment registered there arrived after every condition
/// had been decided against the default. Widest scope first, so the narrowest attribute that
/// answers decides, matching how every other attribute here resolves.
/// </remarks>
private void SeedEnvironment(IServiceCollection serviceCollection, Attribute[] knownAttributes) {
IModuleEnvironment? environment = null;

foreach (var provider in knownAttributes.OfType<IModuleEnvironmentProvider>()) {
environment = provider.ProvideEnvironment(TestMethod.Method) ?? environment;
}

if (environment != null) {
serviceCollection.Add(new ServiceDescriptor(typeof(IModuleEnvironment), environment));
}
}

private void SetupModules(ServiceCollection serviceCollection, IEnumerable<Attribute> knownAttributes) {
var modules = new List<IDependencyModule>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,10 @@ namespace DependencyModules.Runtime.Interfaces
string EnvironmentName { get; }
string? Value(string name);
}
public interface IModuleEnvironmentProvider
{
DependencyModules.Runtime.Interfaces.IModuleEnvironment? ProvideEnvironment(System.Reflection.MethodInfo testMethod);
}
public interface IServiceCollectionConfiguration
{
void ConfigureDecorators(Microsoft.Extensions.DependencyInjection.IServiceCollection services);
Expand Down
Loading