diff --git a/.github/workflows/build-package.yaml b/.github/workflows/build-package.yaml
index 2c8632c..5c88f41 100644
--- a/.github/workflows/build-package.yaml
+++ b/.github/workflows/build-package.yaml
@@ -70,6 +70,11 @@ jobs:
name: coverage-report
path: artifacts/coverage
+ # The coverage run is on xunit.v3 3.x. This runs the xUnit tests again on 4.x, against
+ # DependencyModules.xUnit4.
+ - name: Test on xunit.v3 4.x
+ run: ./scripts/test-xunit.sh 4
+
# Packs and consumes the packages the way a real user would. Catches packaging faults
# that a ProjectReference-based test suite cannot see.
- name: Verify packages
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index ca1636a..b417d71 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -74,6 +74,9 @@ jobs:
cat artifacts/coverage/SummaryGithub.md >> "$GITHUB_STEP_SUMMARY"
fi
+ - name: Test on xunit.v3 4.x
+ run: ./scripts/test-xunit.sh 4
+
- name: Verify packages
run: ./scripts/verify-packages.sh "${{ steps.version.outputs.version }}"
@@ -86,6 +89,7 @@ jobs:
src/DependencyModules.SourceGenerator.Impl/DependencyModules.SourceGenerator.Impl.csproj \
src/DependencyModules.Testing/DependencyModules.Testing.csproj \
src/DependencyModules.xUnit/DependencyModules.xUnit.csproj \
+ src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj \
src/DependencyModules.NUnit/DependencyModules.NUnit.csproj \
src/DependencyModules.NSubstitute/DependencyModules.NSubstitute.csproj \
src/DependencyModules.Moq/DependencyModules.Moq.csproj \
diff --git a/.github/workflows/xunit-prerelease.yaml b/.github/workflows/xunit-prerelease.yaml
new file mode 100644
index 0000000..477dec1
--- /dev/null
+++ b/.github/workflows/xunit-prerelease.yaml
@@ -0,0 +1,50 @@
+name: xunit-prerelease
+
+# Runs the xUnit tests against the newest xunit.v3 on nuget.org, prereleases included. xUnit ships
+# prereleases of a major for months before the release, and the 4.0 breaks were in them: the first
+# on 2026-04-12, four months before 4.0.0. A failure here is notice of a coming break. Without it,
+# the first report comes from a consumer.
+on:
+ schedule:
+ - cron: '0 6 * * 1'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # See build-package.yaml: the 10.0 SDK builds, the 8.0 runtime runs the net8.0 tests.
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
+
+ - name: Find the newest xunit.v3 and runner
+ id: versions
+ run: |
+ newest() {
+ curl -fsSL "https://api.nuget.org/v3-flatcontainer/$1/index.json" | python3 -c '
+ import json, sys
+
+ def precedence(version):
+ release, _, prerelease = version.partition("-")
+ numbers = [int(part) for part in release.split(".")]
+ if not prerelease:
+ return (numbers, 1, [])
+ return (numbers, 0, [(0, int(part), "") if part.isdigit() else (1, 0, part) for part in prerelease.split(".")])
+
+ print(max(json.load(sys.stdin)["versions"], key=precedence))
+ '
+ }
+ echo "xunit=$(newest xunit.v3.mtp-off)" >> "$GITHUB_OUTPUT"
+ echo "runner=$(newest xunit.runner.visualstudio)" >> "$GITHUB_OUTPUT"
+
+ # 4 is the newest DependencyModules xUnit build. Raise it when a build for a new major is added.
+ - name: Test on xunit.v3.mtp-off ${{ steps.versions.outputs.xunit }}
+ run: ./scripts/test-xunit.sh 4 "${{ steps.versions.outputs.xunit }}" "${{ steps.versions.outputs.runner }}"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8f9a9b4..427ef57 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,7 +22,15 @@ To run all tests with code coverage, use the coverage script. The script writes
./scripts/coverage.sh 85
```
-To do a test of the packages that a user gets, use the package script. The script packs the nine packages. Then it builds and runs a test application for each target framework. The test application references these packages.
+The xUnit test projects use `xunit.v3` version 3 and `DependencyModules.xUnit`. To run them with `xunit.v3` version 4 and `DependencyModules.xUnit4`, use the xUnit script:
+
+```shell
+./scripts/test-xunit.sh 4
+```
+
+The `XunitMajor` property in `Directory.Build.props` selects the version. The script sets it to the major version that you give.
+
+To do a test of the packages that a user gets, use the package script. The script packs the ten packages. Then it builds and runs a test application for each target framework. The test application references these packages.
```shell
./scripts/verify-packages.sh
@@ -63,7 +71,10 @@ The `build-package` workflow runs for each pull request to `main`. It does these
1. It does a check of the format with CSharpier.
2. It builds the solution.
3. It runs all tests with code coverage. The line coverage must be 85 percent or more.
-4. It runs `scripts/verify-packages.sh`.
+4. It runs the xUnit tests again with `xunit.v3` version 4.
+5. It runs `scripts/verify-packages.sh`.
+
+The `xunit-prerelease` workflow runs each week. It runs the xUnit tests with the newest `xunit.v3` on nuget.org, prereleases included. A failure tells you about a change in xUnit before its release.
## Documentation
@@ -96,8 +107,8 @@ git push origin v1.5.0
The `release` workflow then does these steps:
1. It builds the code.
-2. It runs the tests.
-3. It packs the nine packages.
+2. It runs the tests with `xunit.v3` version 3 and version 4.
+3. It packs the ten packages.
4. It publishes the packages to nuget.org and to GitHub Packages.
5. It makes a GitHub release with generated release notes.
diff --git a/DependencyModules.sln b/DependencyModules.sln
index 19d4acb..61573f4 100644
--- a/DependencyModules.sln
+++ b/DependencyModules.sln
@@ -46,6 +46,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.NUnit", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SutProject.NUnitTests", "integ-tests\SutProject.NUnitTests\SutProject.NUnitTests.csproj", "{C3B5B8CE-D219-42C8-A1B8-5C283C053E98}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyModules.xUnit4", "src\DependencyModules.xUnit4\DependencyModules.xUnit4.csproj", "{D1AEA1C9-CD76-4664-956B-706962E78E3D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -272,6 +274,18 @@ Global
{C3B5B8CE-D219-42C8-A1B8-5C283C053E98}.Release|x64.Build.0 = Release|Any CPU
{C3B5B8CE-D219-42C8-A1B8-5C283C053E98}.Release|x86.ActiveCfg = Release|Any CPU
{C3B5B8CE-D219-42C8-A1B8-5C283C053E98}.Release|x86.Build.0 = Release|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|x64.Build.0 = Debug|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Debug|x86.Build.0 = Debug|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|x64.ActiveCfg = Release|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|x64.Build.0 = Release|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|x86.ActiveCfg = Release|Any CPU
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -296,5 +310,6 @@ Global
{DB6B8603-CBA7-443D-AEA9-E37BF61AD1B7} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
{F911E2B7-4D5A-4DC2-9E3E-4B2CFC30CF4E} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
{C3B5B8CE-D219-42C8-A1B8-5C283C053E98} = {DC7C95FE-F58F-4F16-8DFA-87E1B67DCEAE}
+ {D1AEA1C9-CD76-4664-956B-706962E78E3D} = {FB161494-E422-4D4E-BA04-3473C2EB13B0}
EndGlobalSection
EndGlobal
diff --git a/Directory.Build.props b/Directory.Build.props
index 7799100..f17ff18 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -35,6 +35,31 @@
net8.0;net10.0
+
+
+ 3
+
+
+
+ xunit.v3
+ 3.2.2
+ 3.1.5
+ DependencyModules.xUnit
+
+
+
+ xunit.v3.mtp-off
+ 4.0.1
+ 4.0.0
+ DependencyModules.xUnit4
+
+
Ian JohnsonIan Johnson
diff --git a/README.md b/README.md
index 27de41f..050a75d 100644
--- a/README.md
+++ b/README.md
@@ -107,9 +107,10 @@ The `integ-tests` folder contains projects that reference the source projects of
| --- | --- |
| [DependencyModules.Runtime](https://www.nuget.org/packages/DependencyModules.Runtime/) | The attributes, the interfaces, and the `AddModule` methods. |
| [DependencyModules.SourceGenerator](https://www.nuget.org/packages/DependencyModules.SourceGenerator/) | The source generator. It operates only when you compile. |
-| [DependencyModules.xUnit](https://www.nuget.org/packages/DependencyModules.xUnit/) | `[ModuleTest]` for xUnit v3. |
+| [DependencyModules.xUnit](https://www.nuget.org/packages/DependencyModules.xUnit/) | `[ModuleTest]` for xUnit v3, with `xunit.v3` version 3. |
+| [DependencyModules.xUnit4](https://www.nuget.org/packages/DependencyModules.xUnit4/) | `[ModuleTest]` for xUnit v3, with `xunit.v3` version 4. |
| [DependencyModules.NUnit](https://www.nuget.org/packages/DependencyModules.NUnit/) | `[ModuleTest]` and `[ModuleTestCase]` for NUnit 4. |
-| [DependencyModules.Testing](https://www.nuget.org/packages/DependencyModules.Testing/) | The test attributes and interfaces that the xUnit and NUnit packages use. |
+| [DependencyModules.Testing](https://www.nuget.org/packages/DependencyModules.Testing/) | The test attributes and interfaces that the xUnit and NUnit packages use. It also gives the test that runs, and a logger that writes to the output of the test. |
| [DependencyModules.NSubstitute](https://www.nuget.org/packages/DependencyModules.NSubstitute/) | `[Mock]` parameters with NSubstitute. |
| [DependencyModules.Moq](https://www.nuget.org/packages/DependencyModules.Moq/) | `[Mock]` and `Mock` parameters with Moq. |
| [DependencyModules.FakeItEasy](https://www.nuget.org/packages/DependencyModules.FakeItEasy/) | `[Mock]` parameters with FakeItEasy. |
diff --git a/integ-tests/SutProject.NUnitTests/CurrentTestTests.cs b/integ-tests/SutProject.NUnitTests/CurrentTestTests.cs
new file mode 100644
index 0000000..3dd02eb
--- /dev/null
+++ b/integ-tests/SutProject.NUnitTests/CurrentTestTests.cs
@@ -0,0 +1,101 @@
+using DependencyModules.NUnit.Attributes;
+using DependencyModules.Testing.Attributes.Interfaces;
+using DependencyModules.Testing.Impl;
+using Microsoft.Extensions.Logging;
+using NUnit.Framework;
+using NUnit.Framework.Internal;
+
+namespace SutProject.NUnitTests;
+
+///
+/// CurrentTest under NUnit, where the runner package answers from
+/// TestExecutionContext.CurrentContext.
+///
+public class CurrentTestTests
+{
+ private static readonly List
+
+
diff --git a/src/DependencyModules.Testing/Impl/CurrentTest.cs b/src/DependencyModules.Testing/Impl/CurrentTest.cs
new file mode 100644
index 0000000..9960a22
--- /dev/null
+++ b/src/DependencyModules.Testing/Impl/CurrentTest.cs
@@ -0,0 +1,39 @@
+using System.Reflection;
+using DependencyModules.Testing.Attributes.Interfaces;
+
+namespace DependencyModules.Testing.Impl;
+
+///
+/// The running test, read through the that the runner package
+/// installed.
+///
+///
+///
+/// DependencyModules.xUnit, DependencyModules.xUnit4 and DependencyModules.NUnit install their
+/// provider before the first of their tests runs. Without a runner package is
+/// null, and every member answers as if no test were running.
+///
+///
+/// Settable, so that a test of code which reads this can put a provider of its own in place. A runner
+/// installs its provider only when none is set.
+///
+///
+public static class CurrentTest
+{
+ ///
+ /// The provider every member reads through, or null when no runner package has installed one.
+ ///
+ public static ICurrentTestProvider? Provider { get; set; }
+
+ ///
+ public static object? Key => Provider?.Key;
+
+ ///
+ public static string? DisplayName => Provider?.DisplayName;
+
+ ///
+ public static Assembly? Assembly => Provider?.Assembly;
+
+ ///
+ public static bool TryWriteLine(string message) => Provider?.TryWriteLine(message) ?? false;
+}
diff --git a/src/DependencyModules.Testing/Impl/TestOutputLoggerProvider.cs b/src/DependencyModules.Testing/Impl/TestOutputLoggerProvider.cs
new file mode 100644
index 0000000..0b4b7f2
--- /dev/null
+++ b/src/DependencyModules.Testing/Impl/TestOutputLoggerProvider.cs
@@ -0,0 +1,119 @@
+using System.Collections.Concurrent;
+using System.Text;
+using DependencyModules.Testing.Attributes.Interfaces;
+using Microsoft.Extensions.Logging;
+
+namespace DependencyModules.Testing.Impl;
+
+///
+/// A logger provider that writes each entry to the output of the running test.
+///
+///
+///
+/// An entry is one line in the console logger's single-line format,
+/// info: Shop.OrderService[0] Order 42 accepted, with an exception on the lines after it.
+/// An entry logged while no test is running is dropped, because there is no output to write it to.
+///
+///
+/// Registered in a test's container as an , it puts the log of the
+/// application under test into the test's output. The container's logger factory writes to it
+/// beside any other provider.
+///
+///
+public sealed class TestOutputLoggerProvider : ILoggerProvider
+{
+ private readonly ICurrentTestProvider? _currentTest;
+
+ private readonly ConcurrentDictionary _loggers = new(
+ StringComparer.Ordinal
+ );
+
+ ///
+ /// Writes through , read again for each entry.
+ ///
+ public TestOutputLoggerProvider() { }
+
+ ///
+ /// Writes through .
+ ///
+ public TestOutputLoggerProvider(ICurrentTestProvider currentTest)
+ {
+ ArgumentNullException.ThrowIfNull(currentTest);
+
+ _currentTest = currentTest;
+ }
+
+ ///
+ public ILogger CreateLogger(string categoryName) =>
+ _loggers.GetOrAdd(categoryName, name => new TestOutputLogger(name, this));
+
+ ///
+ public void Dispose() { }
+
+ private void Write(string entry) => (_currentTest ?? CurrentTest.Provider)?.TryWriteLine(entry);
+
+ private sealed class TestOutputLogger(string categoryName, TestOutputLoggerProvider provider)
+ : ILogger
+ {
+ public IDisposable? BeginScope(TState state)
+ where TState : notnull => null;
+
+ public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
+
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception? exception,
+ Func formatter
+ )
+ {
+ ArgumentNullException.ThrowIfNull(formatter);
+
+ if (!IsEnabled(logLevel))
+ {
+ return;
+ }
+
+ var message = formatter(state, exception);
+
+ // What the console logger does with an entry that has nothing to show.
+ if (string.IsNullOrEmpty(message) && exception == null)
+ {
+ return;
+ }
+
+ var entry = new StringBuilder()
+ .Append(LevelName(logLevel))
+ .Append(": ")
+ .Append(categoryName)
+ .Append('[')
+ .Append(eventId.Id)
+ .Append(']');
+
+ if (!string.IsNullOrEmpty(message))
+ {
+ entry.Append(' ').Append(message);
+ }
+
+ if (exception != null)
+ {
+ entry.AppendLine().Append(exception);
+ }
+
+ provider.Write(entry.ToString());
+ }
+
+ private static string LevelName(LogLevel logLevel) =>
+ logLevel switch
+ {
+ LogLevel.Trace => "trce",
+ LogLevel.Debug => "dbug",
+ LogLevel.Information => "info",
+ LogLevel.Warning => "warn",
+ LogLevel.Error => "fail",
+ LogLevel.Critical => "crit",
+ _ => logLevel.ToString(),
+ };
+ }
+}
diff --git a/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs b/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs
index d76a20c..2c8c444 100644
--- a/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs
+++ b/src/DependencyModules.xUnit/Attributes/ModuleTestAttribute.cs
@@ -23,6 +23,9 @@ namespace DependencyModules.xUnit.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public class ModuleTestAttribute : FactAttribute, IModuleTestAttribute
{
+ // xUnit creates the attribute while it discovers the tests, before the first one runs.
+ static ModuleTestAttribute() => XunitCurrentTestProvider.Install();
+
///
/// Marks a test method, taking no modules.
///
diff --git a/src/DependencyModules.xUnit/DependencyModules.xUnit.csproj b/src/DependencyModules.xUnit/DependencyModules.xUnit.csproj
index c54c3b0..80cefae 100644
--- a/src/DependencyModules.xUnit/DependencyModules.xUnit.csproj
+++ b/src/DependencyModules.xUnit/DependencyModules.xUnit.csproj
@@ -6,7 +6,7 @@
enableTrueDependencyModules.xUnit
- xUnit v3 support for DependencyModules. [ModuleTest] builds a service provider from your modules for each test and each data row. The test parameters get their values from this service provider.
+ xUnit v3 support for DependencyModules, for xunit.v3 version 3. [ModuleTest] builds a service provider from your modules for each test and each data row. The test parameters get their values from this service provider. For xunit.v3 version 4, use DependencyModules.xUnit4.true
@@ -22,17 +22,20 @@
diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestCase.XunitMajor.cs b/src/DependencyModules.xUnit/Impl/ModuleTestCase.XunitMajor.cs
new file mode 100644
index 0000000..f502ed2
--- /dev/null
+++ b/src/DependencyModules.xUnit/Impl/ModuleTestCase.XunitMajor.cs
@@ -0,0 +1,158 @@
+using Xunit.Sdk;
+using Xunit.v3;
+
+namespace DependencyModules.xUnit.Impl;
+
+// The members of ModuleTestCase whose xUnit signatures differ between xunit.v3 majors, and nothing
+// else. DependencyModules.xUnit compiles this file against 3.x. DependencyModules.xUnit4 compiles the
+// same sources against 4.x with XUNIT_V4 defined. A later major adds a branch here and a project of
+// its own.
+public partial class ModuleTestCase
+{
+#if XUNIT_V4
+ ///
+ /// Runs the case the way xUnit would have, and disposes every container it built once the
+ /// run has returned - the tests passed, failed, were skipped or were cancelled alike.
+ ///
+ ///
+ /// XunitRunnerHelper.RunXunitTestCase is what the method runner calls for a case that
+ /// does not execute itself: it creates the tests, turns a failure or a dynamic skip during
+ /// creation into the case's result, and hands the tests to .
+ /// Wrapping that call is the whole of the difference. Disposal is per case, which for every
+ /// test but a data-driven one is per test; the rows of a data-driven test share the case and
+ /// are released together when the last has run.
+ ///
+ public async ValueTask Run(
+ ExplicitOption explicitOption,
+ IMessageBus messageBus,
+ object?[] constructorArguments,
+ ExceptionAggregator aggregator,
+ CancellationTokenSource cancellationTokenSource,
+ ParallelMode parallelMode,
+ ExecutionScheduler scheduler,
+ FixtureMappingManager methodFixtureMappings
+ )
+ {
+ try
+ {
+ return await XunitRunnerHelper.RunXunitTestCase(
+ testCase: this,
+ messageBus: messageBus,
+ cancellationTokenSource: cancellationTokenSource,
+ parallelMode: parallelMode,
+ scheduler: scheduler,
+ aggregator: aggregator,
+ explicitOption: explicitOption,
+ constructorArguments: constructorArguments,
+ methodFixtureMappings: methodFixtureMappings
+ );
+ }
+ finally
+ {
+ await DisposeProviders();
+ }
+ }
+#else
+ ///
+ /// Runs the case the way xUnit would have, and disposes every container it built once the
+ /// run has returned - the tests passed, failed, were skipped or were cancelled alike.
+ ///
+ ///
+ /// is what the method runner calls for a
+ /// case that does not execute itself: it creates the tests, turns a failure or a dynamic skip
+ /// during creation into the case's result, and hands the tests to
+ /// . Wrapping that call is the whole of the difference.
+ /// Disposal is per case, which for every test but a data-driven one is per test; the rows of
+ /// a data-driven test share the case and are released together when the last has run.
+ ///
+ public async ValueTask Run(
+ ExplicitOption explicitOption,
+ IMessageBus messageBus,
+ object?[] constructorArguments,
+ ExceptionAggregator aggregator,
+ CancellationTokenSource cancellationTokenSource
+ )
+ {
+ try
+ {
+ return await XunitRunnerHelper.RunXunitTestCase(
+ testCase: this,
+ messageBus: messageBus,
+ cancellationTokenSource: cancellationTokenSource,
+ aggregator: aggregator,
+ explicitOption: explicitOption,
+ constructorArguments: constructorArguments
+ );
+ }
+ finally
+ {
+ await DisposeProviders();
+ }
+ }
+#endif
+
+ ///
+ /// Creates one test of this case from values already resolved against the case.
+ ///
+ ///
+ /// and are the data row the test comes
+ /// from and the attribute that gave it, or null for a test without data. 4.x reads a label and
+ /// a parallelization choice from them, which 3.x does not have.
+ ///
+ /// Named throughout: has a second constructor that takes a uniqueID
+ /// string near where this passes testIndex, and positionally the two are told apart by the
+ /// argument's type alone.
+ ///
+ private XunitTest CreateTest(
+ string? skipReason,
+ Type? skipType,
+ string? skipUnless,
+ string? skipWhen,
+ string testDisplayName,
+ int testIndex,
+ IReadOnlyDictionary> traits,
+ int timeout,
+ object?[] testMethodArguments,
+ Xunit.ITheoryDataRow? row,
+ IDataAttribute? dataAttribute
+ )
+ {
+#if XUNIT_V4
+ return new XunitTest(
+ testCase: this,
+ testMethod: TestMethod,
+ @explicit: Explicit,
+ skipReason: skipReason,
+ skipType: skipType,
+ skipUnless: skipUnless,
+ skipWhen: skipWhen,
+ testDisplayName: testDisplayName,
+ testIndex: testIndex,
+ traits: traits,
+ timeout: timeout,
+ testMethodArguments: testMethodArguments,
+ // What [Theory] passes for a row and [Fact] for a test without data. Left to the
+ // obsolete constructor, a row marked DisableParallelization would run in parallel.
+ testLabel: row?.Label ?? TestLabel,
+ disableParallelization: row?.DisableParallelization
+ ?? dataAttribute?.DisableParallelization
+ ?? DisableParallelization
+ );
+#else
+ return new XunitTest(
+ testCase: this,
+ testMethod: TestMethod,
+ @explicit: Explicit,
+ skipReason: skipReason,
+ skipType: skipType,
+ skipUnless: skipUnless,
+ skipWhen: skipWhen,
+ testDisplayName: testDisplayName,
+ testIndex: testIndex,
+ traits: traits,
+ timeout: timeout,
+ testMethodArguments: testMethodArguments
+ );
+#endif
+ }
+}
diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs b/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs
index 461e862..cf0fd70 100644
--- a/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs
+++ b/src/DependencyModules.xUnit/Impl/ModuleTestCase.cs
@@ -22,7 +22,7 @@ namespace DependencyModules.xUnit.Impl;
/// ModuleTestCommand has always disposed in a finally around the test; this is the
/// same lifetime for xUnit.
///
-public class ModuleTestCase : XunitTestCase, ISelfExecutingXunitTestCase
+public partial class ModuleTestCase : XunitTestCase, ISelfExecutingXunitTestCase
{
///
/// One per container this case built: one for a plain test, one per row for a data-driven
@@ -30,6 +30,10 @@ public class ModuleTestCase : XunitTestCase, ISelfExecutingXunitTestCase
///
private readonly List _providers = [];
+ // Here as well as on ModuleTestAttribute, because an attribute of another library can name
+ // ModuleTestDiscoverer, and then this is the first of the package's types that xUnit creates.
+ static ModuleTestCase() => XunitCurrentTestProvider.Install();
+
#pragma warning disable CS0618 // Type or member is obsolete
///
/// Represents a specialized implementation of
@@ -292,43 +296,6 @@ IEnumerable knownAttributes
DependencyRegistry.LoadModules(serviceCollection, modules.ToArray());
}
- ///
- /// Runs the case the way xUnit would have, and disposes every container it built once the
- /// run has returned - the tests passed, failed, were skipped or were cancelled alike.
- ///
- ///
- /// is what the method runner calls for a
- /// case that does not execute itself: it creates the tests, turns a failure or a dynamic skip
- /// during creation into the case's result, and hands the tests to
- /// . Wrapping that call is the whole of the difference.
- /// Disposal is per case, which for every test but a data-driven one is per test; the rows of
- /// a data-driven test share the case and are released together when the last has run.
- ///
- public async ValueTask Run(
- ExplicitOption explicitOption,
- IMessageBus messageBus,
- object?[] constructorArguments,
- ExceptionAggregator aggregator,
- CancellationTokenSource cancellationTokenSource
- )
- {
- try
- {
- return await XunitRunnerHelper.RunXunitTestCase(
- this,
- messageBus,
- cancellationTokenSource,
- aggregator,
- explicitOption,
- constructorArguments
- );
- }
- finally
- {
- await DisposeProviders();
- }
- }
-
private async ValueTask DisposeProviders()
{
var providers = _providers.ToArray();
@@ -419,13 +386,7 @@ IDataAttribute[] dataAttributes
var startupValues = await SetupServiceCollection();
unitTests.Add(
- // testIndex is named for more than readability: XunitTest has a second
- // nine-parameter constructor differing only at this position, taking a uniqueID
- // string. Positionally the two are told apart by the argument's type alone.
- new XunitTest(
- testCase: this,
- testMethod: TestMethod,
- @explicit: Explicit,
+ CreateTest(
skipReason: theoryDataRow.Skip ?? SkipReason,
// The row's own conditional-skip metadata takes precedence over the case's,
// matching how skipReason above already defers to it. These became required
@@ -442,7 +403,9 @@ IDataAttribute[] dataAttributes
.GetTraits(TestMethod, theoryDataRow)
.ToReadOnlyTraits(),
timeout: theoryDataRow.Timeout ?? Timeout,
- testMethodArguments: await ResolveArguments(data, startupValues)
+ testMethodArguments: await ResolveArguments(data, startupValues),
+ row: theoryDataRow,
+ dataAttribute: dataAttribute
)
);
}
@@ -513,10 +476,7 @@ private async Task> UnitTestWithNoDataAttributes
return
[
- new XunitTest(
- testCase: this,
- testMethod: TestMethod,
- @explicit: Explicit,
+ CreateTest(
skipReason: SkipReason,
skipType: SkipType,
skipUnless: SkipUnless,
@@ -525,7 +485,9 @@ private async Task> UnitTestWithNoDataAttributes
testIndex: 0,
traits: Traits.ToReadOnlyTraits(),
timeout: Timeout,
- testMethodArguments: await ResolveArguments([], startupValues)
+ testMethodArguments: await ResolveArguments([], startupValues),
+ row: null,
+ dataAttribute: null
),
];
}
diff --git a/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs b/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs
index f49adad..c33d44d 100644
--- a/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs
+++ b/src/DependencyModules.xUnit/Impl/ModuleTestDiscoverer.cs
@@ -47,7 +47,8 @@ IFactAttribute factAttribute
// second GetTestCaseDetails taking a trailing label, and since every added parameter on
// both is optional, a three-argument call matches the two equally well and is ambiguous.
// Naming a parameter only the newer one declares resolves it. A module test has no label,
- // which is what null says.
+ // which is what null says. 4.x has only the one overload, with displayNameIndex added, and
+ // the DependencyModules.xUnit4 build binds the same call to it.
var details = TestIntrospectionHelper.GetTestCaseDetails(
discoveryOptions,
testMethod,
diff --git a/src/DependencyModules.xUnit/Impl/XunitCurrentTestProvider.cs b/src/DependencyModules.xUnit/Impl/XunitCurrentTestProvider.cs
new file mode 100644
index 0000000..a14eda0
--- /dev/null
+++ b/src/DependencyModules.xUnit/Impl/XunitCurrentTestProvider.cs
@@ -0,0 +1,58 @@
+using System.Reflection;
+using DependencyModules.Testing.Attributes.Interfaces;
+using DependencyModules.Testing.Impl;
+using Xunit;
+using Xunit.v3;
+
+namespace DependencyModules.xUnit.Impl;
+
+///
+/// Reads the running test from xUnit's .
+///
+///
+///
+/// Installed from the static constructors of and of
+/// ModuleTestAttribute. xUnit creates both while it discovers or deserializes the tests,
+/// before the first one runs.
+///
+///
+/// xUnit has no running test while a case builds its containers: the case creates its tests, and
+/// the container for each, before xUnit runs the first of them. So and
+/// are null in the hooks that build a container, and a line written there
+/// is dropped. answers there, from the test class.
+///
+///
+internal sealed class XunitCurrentTestProvider : ICurrentTestProvider
+{
+ public object? Key => TestContext.Current.Test;
+
+ public string? DisplayName => TestContext.Current.Test?.TestDisplayName;
+
+ public Assembly? Assembly =>
+ TestContext.Current.TestClass is IXunitTestClass testClass
+ ? testClass.Class.Assembly
+ : null;
+
+ public bool TryWriteLine(string message)
+ {
+ if (TestContext.Current.TestOutputHelper is not { } output)
+ {
+ return false;
+ }
+
+ try
+ {
+ output.WriteLine(message);
+
+ return true;
+ }
+ // The flow still carries the context of a test that has finished, and xUnit's helper
+ // refuses output that it can no longer report.
+ catch (InvalidOperationException)
+ {
+ return false;
+ }
+ }
+
+ public static void Install() => CurrentTest.Provider ??= new XunitCurrentTestProvider();
+}
diff --git a/src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj b/src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj
new file mode 100644
index 0000000..0a31985
--- /dev/null
+++ b/src/DependencyModules.xUnit4/DependencyModules.xUnit4.csproj
@@ -0,0 +1,54 @@
+
+
+
+ $(LibraryTargetFrameworks)
+ enable
+ enable
+ True
+ DependencyModules.xUnit4
+ $(DefineConstants);XUNIT_V4
+ xUnit v3 support for DependencyModules, for xunit.v3 version 4. [ModuleTest] builds a service provider from your modules for each test and each data row. The test parameters get their values from this service provider. For xunit.v3 version 3, use DependencyModules.xUnit.
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs b/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs
index e28ad66..ea4bae4 100644
--- a/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs
+++ b/tests/DependencyModules.Tests/ApiTests/PublicApiTests.cs
@@ -27,11 +27,23 @@ public void RuntimeApi()
Snapshot.Match(ApiOf(typeof(DependencyModuleAttribute)));
}
+#if XUNIT_V4
+ ///
+ /// DependencyModules.xUnit4, which is the xUnit package's sources built against xunit.v3 4.x.
+ /// The two surfaces differ where the xUnit types they expose differ.
+ ///
+ [Fact]
+ public void XUnit4Api()
+ {
+ Snapshot.Match(ApiOf(typeof(ModuleTestAttribute)));
+ }
+#else
[Fact]
public void XUnitApi()
{
Snapshot.Match(ApiOf(typeof(ModuleTestAttribute)));
}
+#endif
///
/// The NUnit integration. Its [ModuleTest] shares a name with xUnit's and nothing else —
diff --git a/tests/DependencyModules.Tests/DependencyModules.Tests.csproj b/tests/DependencyModules.Tests/DependencyModules.Tests.csproj
index 282c4c7..314533c 100644
--- a/tests/DependencyModules.Tests/DependencyModules.Tests.csproj
+++ b/tests/DependencyModules.Tests/DependencyModules.Tests.csproj
@@ -7,12 +7,17 @@
False
+
+
+ $(DefineConstants);XUNIT_V4
+
+
-
+
+