Browse Source
* Headless NUnit: Fix TearDown not always working * Headless NUnit: Handle async SetUp/TearDownbug/focus-within-not-cleared
committed by
GitHub
4 changed files with 245 additions and 48 deletions
@ -0,0 +1,86 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Runtime.CompilerServices; |
|||
using NUnit.Framework.Interfaces; |
|||
using NUnit.Framework.Internal; |
|||
using NUnit.Framework.Internal.Commands; |
|||
|
|||
namespace Avalonia.Headless.NUnit; |
|||
|
|||
/// <summary>
|
|||
/// 2023-05-10, original comment from Max about NUnit 3:
|
|||
/// There are multiple problems with NUnit integration at the moment when we wrote this integration.
|
|||
/// NUnit doesn't have extensibility API for running on custom dispatcher/sync-context.
|
|||
/// See https://github.com/nunit/nunit/issues/2917 https://github.com/nunit/nunit/issues/2774
|
|||
/// To workaround that we had to replace inner TestMethodCommand with our own implementation while keeping original hierarchy of commands.
|
|||
/// Which will respect proper async/await awaiting code that works with our session and can be block-awaited to fit in NUnit.
|
|||
/// Also, we need to push BeforeTest/AfterTest callbacks to the very same session call.
|
|||
/// I hope there will be a better solution without reflection, but for now that's it.
|
|||
///
|
|||
/// 2026-02-04: the situation hasn't changed at all with NUnit 4.
|
|||
/// </summary>
|
|||
internal static class NUnitReflectionHelper |
|||
{ |
|||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = ReflectionDelegatingTestCommand.InnerCommandFieldName)] |
|||
private static extern ref TestCommand DelegatingTestCommand_InnerCommand(DelegatingTestCommand instance); |
|||
|
|||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = ReflectionBeforeAndAfterTestCommand.BeforeTestFieldName)] |
|||
private static extern ref Action<TestExecutionContext>? BeforeAndAfterTestCommand_BeforeTest(BeforeAndAfterTestCommand instance); |
|||
|
|||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = ReflectionBeforeAndAfterTestCommand.AfterTestFieldName)] |
|||
private static extern ref Action<TestExecutionContext>? BeforeAndAfterTestCommand_AfterTest(BeforeAndAfterTestCommand instance); |
|||
|
|||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_setUpMethods")] |
|||
private static extern ref IList<IMethodInfo> SetUpTearDownItem_SetUpMethods(SetUpTearDownItem instance); |
|||
|
|||
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_tearDownMethods")] |
|||
private static extern ref IList<IMethodInfo> SetUpTearDownItem_TearDownMethods(SetUpTearDownItem instance); |
|||
|
|||
extension(DelegatingTestCommand instance) |
|||
{ |
|||
public ref TestCommand InnerCommand() |
|||
=> ref DelegatingTestCommand_InnerCommand(instance); |
|||
} |
|||
|
|||
extension(BeforeAndAfterTestCommand instance) |
|||
{ |
|||
public ref Action<TestExecutionContext>? BeforeTest() |
|||
=> ref BeforeAndAfterTestCommand_BeforeTest(instance); |
|||
|
|||
public ref Action<TestExecutionContext>? AfterTest() |
|||
=> ref BeforeAndAfterTestCommand_AfterTest(instance); |
|||
} |
|||
|
|||
extension(SetUpTearDownItem instance) |
|||
{ |
|||
public ref IList<IMethodInfo> SetUpMethods() |
|||
=> ref SetUpTearDownItem_SetUpMethods(instance); |
|||
|
|||
public ref IList<IMethodInfo> TearDownMethods() |
|||
=> ref SetUpTearDownItem_TearDownMethods(instance); |
|||
} |
|||
|
|||
private sealed class ReflectionDelegatingTestCommand : DelegatingTestCommand |
|||
{ |
|||
public ReflectionDelegatingTestCommand(TestCommand innerCommand) |
|||
: base(innerCommand) |
|||
{ |
|||
} |
|||
|
|||
public const string InnerCommandFieldName = nameof(innerCommand); |
|||
|
|||
public override TestResult Execute(TestExecutionContext context) |
|||
=> throw new NotSupportedException("Reflection-only type, this method should never be called"); |
|||
} |
|||
|
|||
private sealed class ReflectionBeforeAndAfterTestCommand : BeforeAndAfterTestCommand |
|||
{ |
|||
public ReflectionBeforeAndAfterTestCommand(TestCommand innerCommand) |
|||
: base(innerCommand) |
|||
{ |
|||
} |
|||
|
|||
public const string BeforeTestFieldName = nameof(BeforeTest); |
|||
public const string AfterTestFieldName = nameof(AfterTest); |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
#if NUNIT
|
|||
|
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Headless.UnitTests; |
|||
|
|||
public class AsyncSetupTests |
|||
{ |
|||
private static int s_instanceCount; |
|||
|
|||
[SetUp] |
|||
public async Task SetUp() |
|||
{ |
|||
await Task.Delay(100); |
|||
++s_instanceCount; |
|||
} |
|||
|
|||
[AvaloniaTest, TestCase(1), TestCase(2)] |
|||
public void Async_Setup_TearDown_Should_Work(int index) |
|||
{ |
|||
AssertHelper.Equal(1, s_instanceCount); |
|||
} |
|||
|
|||
[TearDown] |
|||
public async Task TearDown() |
|||
{ |
|||
await Task.Delay(100); |
|||
--s_instanceCount; |
|||
} |
|||
} |
|||
|
|||
#endif
|
|||
@ -0,0 +1,45 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Headless.UnitTests; |
|||
|
|||
public class SetupTests |
|||
#if XUNIT
|
|||
: IDisposable |
|||
#endif
|
|||
{ |
|||
private static int s_instanceCount; |
|||
|
|||
#if NUNIT
|
|||
[SetUp] |
|||
public void SetUp() |
|||
#elif XUNIT
|
|||
public SetupTests() |
|||
#endif
|
|||
{ |
|||
++s_instanceCount; |
|||
} |
|||
|
|||
#if NUNIT
|
|||
[AvaloniaTest, TestCase(1), TestCase(2)] |
|||
#elif XUNIT
|
|||
[AvaloniaTheory, InlineData(1), InlineData(2)] |
|||
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage( |
|||
"Usage", |
|||
"xUnit1026:Theory methods should use all of their parameters", |
|||
Justification = "Used to run the test several times")] |
|||
#endif
|
|||
public void Setup_TearDown_Should_Work(int index) |
|||
{ |
|||
AssertHelper.Equal(1, s_instanceCount); |
|||
} |
|||
|
|||
#if NUNIT
|
|||
[TearDown] |
|||
public void TearDown() |
|||
#elif XUNIT
|
|||
public void Dispose() |
|||
#endif
|
|||
{ |
|||
--s_instanceCount; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue