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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ The `integ-tests` folder contains projects that reference the source projects of
| [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<T>` parameters with Moq. |
| [DependencyModules.FakeItEasy](https://www.nuget.org/packages/DependencyModules.FakeItEasy/) | `[Mock]` parameters with FakeItEasy. |
Expand Down
101 changes: 101 additions & 0 deletions integ-tests/SutProject.NUnitTests/CurrentTestTests.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// <c>CurrentTest</c> under NUnit, where the runner package answers from
/// <c>TestExecutionContext.CurrentContext</c>.
/// </summary>
public class CurrentTestTests
{
private static readonly List<object?> RepeatKeys = [];

[ModuleTest]
public void KeyIsNUnitsObjectForTheRunningTest() =>
Assert.That(CurrentTest.Key, Is.SameAs(TestExecutionContext.CurrentContext.CurrentTest));

[ModuleTest]
public void DisplayNameIsTheFullNameOfTheTest() =>
Assert.That(CurrentTest.DisplayName, Is.EqualTo(TestContext.CurrentContext.Test.FullName));

[ModuleTest]
public void AssemblyIsTheAssemblyOfTheTestClass() =>
Assert.That(CurrentTest.Assembly, Is.SameAs(typeof(CurrentTestTests).Assembly));

[ModuleTest]
public void ALineIsWrittenToTheOutputOfTheTest()
{
Assert.That(CurrentTest.TryWriteLine("written through CurrentTest"), Is.True);

Assert.That(
TestExecutionContext.CurrentContext.CurrentResult.Output,
Does.Contain("written through CurrentTest")
);
}

[ModuleTest]
public void TheLoggerProviderWritesToTheOutputOfTheTest()
{
new TestOutputLoggerProvider()
.CreateLogger("Shop.OrderService")
.LogInformation("Order {Id} accepted", 42);

Assert.That(
TestExecutionContext.CurrentContext.CurrentResult.Output,
Does.Contain("info: Shop.OrderService[0] Order 42 accepted")
);
}

/// <summary>
/// The container is built inside the iteration, so the test is already running in the hooks
/// that build it. Under xUnit it is not; the documentation of CurrentTest gives both.
/// </summary>
[ModuleTest]
[RecordCurrentTestAtStartup]
public void TheTestIsRunningWhileTheContainerIsBuilt()
{
Assert.That(RecordCurrentTestAtStartupAttribute.Key, Is.SameAs(CurrentTest.Key));
Assert.That(
TestExecutionContext.CurrentContext.CurrentResult.Output,
Does.Contain("written while the container is built")
);
}

/// <summary>
/// NUnit makes no object of its own for one iteration, so the key is the test's for all of
/// them. The documentation of CurrentTest says so; the container is still one per iteration.
/// </summary>
[ModuleTest]
[Repeat(2)]
public void TheIterationsOfARepeatedTestShareOneKey()
{
RepeatKeys.Add(CurrentTest.Key);

Assert.That(CurrentTest.Key, Is.Not.Null);
Assert.That(
RepeatKeys,
Has.Count.EqualTo(TestExecutionContext.CurrentContext.CurrentRepeatCount + 1)
);
Assert.That(RepeatKeys, Is.All.SameAs(RepeatKeys[0]));
}
}

[AttributeUsage(AttributeTargets.Method)]
public sealed class RecordCurrentTestAtStartupAttribute : Attribute, ITestStartupAttribute
{
public static object? Key { get; private set; }

public Task StartupAsync(ITestMethodContext testMethod, IServiceProvider serviceProvider)
{
Key = CurrentTest.Key;

CurrentTest.TryWriteLine("written while the container is built");

return Task.CompletedTask;
}
}
167 changes: 167 additions & 0 deletions integ-tests/SutProject.Tests/TestFramework/CurrentTestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using System.Reflection;
using DependencyModules.Testing.Attributes.Interfaces;
using DependencyModules.Testing.Impl;
using DependencyModules.xUnit.Attributes;
using Microsoft.Extensions.Logging;
using Xunit;

namespace SutProject.Tests.TestFramework;

/// <summary>
/// <c>CurrentTest</c> under xUnit, where the runner package answers from <c>TestContext.Current</c>.
/// </summary>
/// <remarks>
/// SutProject.Tests runs against both xunit.v3 majors, so these hold for DependencyModules.xUnit and
/// for DependencyModules.xUnit4.
/// </remarks>
public class CurrentTestTests
{
private static readonly Dictionary<int, object> RowKeys = [];

[ModuleTest]
public void KeyIsXunitsObjectForTheRunningTest() =>
Assert.Same(TestContext.Current.Test, CurrentTest.Key);

[ModuleTest]
public void DisplayNameIsXunitsDisplayName() =>
Assert.Equal(TestContext.Current.Test!.TestDisplayName, CurrentTest.DisplayName);

[ModuleTest]
public void AssemblyIsTheAssemblyOfTheTestClass() =>
Assert.Same(typeof(CurrentTestTests).Assembly, CurrentTest.Assembly);

[ModuleTest]
public void ALineIsWrittenToTheOutputOfTheTest()
{
Assert.True(CurrentTest.TryWriteLine("written through CurrentTest"));

Assert.Contains(
"written through CurrentTest",
TestContext.Current.TestOutputHelper!.Output
);
}

[ModuleTest]
public void TheLoggerProviderWritesToTheOutputOfTheTest()
{
new TestOutputLoggerProvider()
.CreateLogger("Shop.OrderService")
.LogInformation("Order {Id} accepted", 42);

Assert.Contains(
"info: Shop.OrderService[0] Order 42 accepted",
TestContext.Current.TestOutputHelper!.Output
);
}

/// <summary>
/// Each row is a test of its own, so per-test state keyed on one row does not reach another.
/// </summary>
[ModuleTest]
[InlineData(1)]
[InlineData(2)]
public void EachDataRowHasAKeyOfItsOwn(int row)
{
var key = Assert.IsAssignableFrom<object>(CurrentTest.Key);

lock (RowKeys)
{
Assert.DoesNotContain(RowKeys.Values, earlier => ReferenceEquals(earlier, key));

RowKeys[row] = key;
}
}

/// <summary>
/// A background flow keeps the context of the test that started it after that test finishes,
/// and xUnit's output helper then throws when written to. A logger on such a flow must drop
/// the line and not throw into the application under test.
/// </summary>
/// <remarks>
/// Two tests in one class, which xUnit runs one after the other in an order it does not
/// promise. Whichever runs first starts the writer. The second tells it that the first has
/// finished, and reads what the write returned.
/// </remarks>
[ModuleTest]
public Task ALineWrittenAfterItsTestHasFinishedIsDropped() => WriteAfterTheTestAsync();

[ModuleTest]
public Task WhicheverOfTheTwoLateWriteTestsRanFirst() => WriteAfterTheTestAsync();

private static readonly TaskCompletionSource FirstLateWriteTestFinished = new(
TaskCreationOptions.RunContinuationsAsynchronously
);

private static readonly TaskCompletionSource<bool> LateWrite = new(
TaskCreationOptions.RunContinuationsAsynchronously
);

private static int _lateWriteTestsStarted;

private static async Task WriteAfterTheTestAsync()
{
if (Interlocked.Increment(ref _lateWriteTestsStarted) == 1)
{
_ = Task.Run(async () =>
{
await FirstLateWriteTestFinished.Task;

try
{
LateWrite.SetResult(
CurrentTest.TryWriteLine("written after the test finished")
);
}
catch (Exception exception)
{
LateWrite.SetException(exception);
}
});

return;
}

FirstLateWriteTestFinished.SetResult();

Assert.False(await LateWrite.Task.WaitAsync(TimeSpan.FromSeconds(10)));
}

/// <summary>
/// A case builds the container for each of its tests before xUnit runs the first one, so no
/// test is running yet in the hooks that build it. The documentation of CurrentTest says so.
/// </summary>
[ModuleTest]
[RecordCurrentTestAtStartup]
public void NoTestIsRunningWhileTheContainerIsBuilt()
{
Assert.True(RecordCurrentTestAtStartupAttribute.Recorded);
Assert.Null(RecordCurrentTestAtStartupAttribute.Key);
Assert.False(RecordCurrentTestAtStartupAttribute.Wrote);
Assert.Same(
typeof(CurrentTestTests).Assembly,
RecordCurrentTestAtStartupAttribute.Assembly
);
}
}

[AttributeUsage(AttributeTargets.Method)]
public sealed class RecordCurrentTestAtStartupAttribute : Attribute, ITestStartupAttribute
{
public static bool Recorded { get; private set; }

public static object? Key { get; private set; }

public static Assembly? Assembly { get; private set; }

public static bool Wrote { get; private set; }

public Task StartupAsync(ITestMethodContext testMethod, IServiceProvider serviceProvider)
{
Key = CurrentTest.Key;
Assembly = CurrentTest.Assembly;
Wrote = CurrentTest.TryWriteLine("written while the container is built");
Recorded = true;

return Task.CompletedTask;
}
}
3 changes: 3 additions & 0 deletions src/DependencyModules.NUnit/Attributes/ModuleTestAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ public class ModuleTestAttribute
/// </remarks>
internal const string RowPropertyName = "DependencyModules.ModuleTestRow";

// NUnit creates the attribute while it builds the tests, before the first one runs.
static ModuleTestAttribute() => NUnitCurrentTestProvider.Install();

/// <summary>
/// Marks a test method, optionally naming the modules to configure its container with.
/// </summary>
Expand Down
50 changes: 50 additions & 0 deletions src/DependencyModules.NUnit/Impl/NUnitCurrentTestProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Reflection;
using DependencyModules.Testing.Attributes.Interfaces;
using DependencyModules.Testing.Impl;
using NUnit.Framework;
using NUnit.Framework.Internal;

namespace DependencyModules.NUnit.Impl;

/// <summary>
/// Reads the running test from NUnit's <see cref="TestExecutionContext.CurrentContext"/>.
/// </summary>
/// <remarks>
/// <para>
/// Installed from the static constructor of <c>ModuleTestAttribute</c>. NUnit creates the attribute
/// while it builds the tests, before the first one runs.
/// </para>
/// <para>
/// The key is NUnit's <see cref="Test"/>, which the iterations of a <c>[Repeat]</c> or a
/// <c>[Retry]</c> share: NUnit makes no object of its own for one iteration. The container is still
/// built for each iteration.
/// </para>
/// </remarks>
internal sealed class NUnitCurrentTestProvider : ICurrentTestProvider
{
public object? Key => RunningTest;

public string? DisplayName => RunningTest?.FullName;

public Assembly? Assembly => RunningTest?.TypeInfo?.Assembly;

public bool TryWriteLine(string message)
{
if (RunningTest == null)
{
return false;
}

TestContext.Out.WriteLine(message);

return true;
}

public static void Install() => CurrentTest.Provider ??= new NUnitCurrentTestProvider();

// NUnit answers outside a test with its ad hoc context rather than with null.
private static Test? RunningTest =>
TestExecutionContext.CurrentContext is TestExecutionContext.AdhocContext
? null
: TestExecutionContext.CurrentContext.CurrentTest;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Reflection;

namespace DependencyModules.Testing.Attributes.Interfaces;

/// <summary>
/// Reads the running test from the test framework's own context.
/// </summary>
/// <remarks>
/// <para>
/// The ambient counterpart of <see cref="ITestMethodContext"/>. That one is handed to the hooks that
/// build a test's container. This one answers from anywhere the framework's context flows, such as
/// an assertion helper or a logger, with nothing handed to it. Each runner package installs its own
/// in <c>CurrentTest.Provider</c>: DependencyModules.xUnit and DependencyModules.xUnit4 read
/// <c>TestContext.Current</c>, and DependencyModules.NUnit reads
/// <c>TestExecutionContext.CurrentContext</c>.
/// </para>
/// <para>
/// Every member answers for the test running on the calling flow, and with null or false when there
/// is none. A library that reads this works under any runner package and references no framework.
/// </para>
/// </remarks>
public interface ICurrentTestProvider
{
/// <summary>
/// The framework's own object for the running test, or null when no test is running.
/// </summary>
/// <remarks>
/// Compared by reference. Two tests that run at the same time have different keys, and the
/// framework holds the object for as long as the test runs, so a weak table can hold per-test
/// state against it.
/// </remarks>
object? Key { get; }

/// <summary>
/// The framework's display name for the running test, or null when no test is running.
/// </summary>
string? DisplayName { get; }

/// <summary>
/// The assembly that declares the running test's class, or null when there is no test class.
/// </summary>
Assembly? Assembly { get; }

/// <summary>
/// Writes a line to the running test's output.
/// </summary>
/// <returns>
/// False, having written nothing, when no test is running or the test has finished.
/// </returns>
bool TryWriteLine(string message);
}
Loading