diff --git a/integ-tests/SutProject.NUnitTests/EnvironmentSeedingTests.cs b/integ-tests/SutProject.NUnitTests/EnvironmentSeedingTests.cs
new file mode 100644
index 0000000..80a8cb3
--- /dev/null
+++ b/integ-tests/SutProject.NUnitTests/EnvironmentSeedingTests.cs
@@ -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;
+
+///
+/// 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.
+///
+[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(), Is.Not.Null);
+ }
+
+ ///
+ /// The gate has to hold in the other direction, or the test above passes because the
+ /// condition was never compiled in.
+ ///
+ [ModuleTest(typeof(SeededEnvironmentModule))]
+ public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) {
+ Assert.That(provider.GetService(), Is.Null);
+ }
+}
diff --git a/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentSeedingTests.cs b/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentSeedingTests.cs
new file mode 100644
index 0000000..4f77114
--- /dev/null
+++ b/integ-tests/SutProject.Tests/EnvironmentTests/EnvironmentSeedingTests.cs
@@ -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;
+
+///
+/// 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.
+///
+[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 { }
+
+///
+/// The seeded environment reaches module conditions through the real test runner.
+///
+///
+/// proves the same behaviour for a hand-built
+/// collection through AddModules. These run through [ModuleTest] 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.
+///
+public class EnvironmentSeedingTests {
+
+ [ModuleTest]
+ [SeededEnvironmentModule]
+ [SeededEnvironment("seeded-environment")]
+ public void AGatedRegistrationAppliesUnderTheSeededEnvironment(IServiceProvider provider) {
+ Assert.NotNull(provider.GetService());
+ }
+
+ ///
+ /// The gate has to hold in the other direction, or the test above passes because the
+ /// condition was never compiled in.
+ ///
+ [ModuleTest]
+ [SeededEnvironmentModule]
+ public void TheSameRegistrationIsAbsentWithoutASeed(IServiceProvider provider) {
+ Assert.Null(provider.GetService());
+ }
+
+ /// The environment a module reads is the seeded instance, not a parallel default.
+ [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);
+ }
+}
+
+/// Narrowest scope wins, matching how every other attribute here resolves.
+[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);
+ }
+}
diff --git a/src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs b/src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs
index bd48dbe..4f93cfb 100644
--- a/src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs
+++ b/src/DependencyModules.NUnit/Impl/ModuleTestCommand.cs
@@ -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);
@@ -143,6 +145,25 @@ private static void SetupServiceSetupAttributes(
}
}
+ ///
+ /// 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.
+ ///
+ private static void SeedEnvironment(
+ IServiceCollection serviceCollection, MethodInfo method, Attribute[] knownAttributes) {
+ IModuleEnvironment? environment = null;
+
+ foreach (var provider in knownAttributes.OfType()) {
+ environment = provider.ProvideEnvironment(method) ?? environment;
+ }
+
+ if (environment != null) {
+ serviceCollection.Add(new ServiceDescriptor(typeof(IModuleEnvironment), environment));
+ }
+ }
+
///
/// The same loading the xUnit integration does, reading so
/// neither names the other's attribute. It is not shared code because it needs both
diff --git a/src/DependencyModules.Runtime/Interfaces/IModuleEnvironmentProvider.cs b/src/DependencyModules.Runtime/Interfaces/IModuleEnvironmentProvider.cs
new file mode 100644
index 0000000..a6ac21a
--- /dev/null
+++ b/src/DependencyModules.Runtime/Interfaces/IModuleEnvironmentProvider.cs
@@ -0,0 +1,38 @@
+using System.Reflection;
+
+namespace DependencyModules.Runtime.Interfaces;
+
+///
+/// An attribute that names the environment a test's container is built for, before any module
+/// is applied.
+///
+///
+///
+/// Module registrations are conditioned as they are applied: [IfEnvironment] and its
+/// siblings are answered from the 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.
+///
+///
+/// Beside 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
+/// rather than a test-method context for the same reason.
+///
+///
+/// Attributes are consulted widest scope first - assembly, then class, then method - and the
+/// narrowest one that answers decides, matching how every other attribute resolves.
+///
+///
+public interface IModuleEnvironmentProvider {
+
+ ///
+ /// The environment module conditions are evaluated against, or null to leave the decision
+ /// to a wider scope.
+ ///
+ /// The test method the container is being built for.
+ IModuleEnvironment? ProvideEnvironment(MethodInfo testMethod);
+}
diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs b/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs
index f1548c7..1ac6a98 100644
--- a/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs
+++ b/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs
@@ -84,6 +84,8 @@ private async Task SetupServiceCollection() {
SetupTestCaseInfo(serviceCollection, knownAttributes);
+ SeedEnvironment(serviceCollection, knownAttributes);
+
SetupModules(serviceCollection, knownAttributes);
SetupServiceSetupAttributes(context, serviceCollection, knownAttributes);
@@ -160,6 +162,30 @@ private void SetupServiceSetupAttributes(
}
}
+ ///
+ /// Registers the environment the test's attributes declare, ahead of the modules.
+ ///
+ ///
+ /// Before , because module registrations are conditioned as they are
+ /// applied: LoadModules answers [IfEnvironment] from the
+ /// 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.
+ ///
+ private void SeedEnvironment(IServiceCollection serviceCollection, Attribute[] knownAttributes) {
+ IModuleEnvironment? environment = null;
+
+ foreach (var provider in knownAttributes.OfType()) {
+ environment = provider.ProvideEnvironment(TestMethod.Method) ?? environment;
+ }
+
+ if (environment != null) {
+ serviceCollection.Add(new ServiceDescriptor(typeof(IModuleEnvironment), environment));
+ }
+ }
+
private void SetupModules(ServiceCollection serviceCollection, IEnumerable knownAttributes) {
var modules = new List();
diff --git a/tests/DependencyModules.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt b/tests/DependencyModules.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt
index 8673bfc..7aa4569 100644
--- a/tests/DependencyModules.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt
+++ b/tests/DependencyModules.Tests/Snapshots/PublicApiTests.RuntimeApi.verified.txt
@@ -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);