Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Avalonia.Headless.NUnit] Make SetUp and TearDown methods work when async #18306

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

jonko0493
Copy link
Contributor

@jonko0493 jonko0493 commented Feb 24, 2025

What does the pull request do?

Makes the SetUp and TearDown methods in NUnit work within Avalonia test fixtures.

What is the current behavior?

When attempting to use [SetUp] or [TearDown] attributes within an Avalonia.Headless.NUnit project, the tests will run forever/until timeout due to improper handling of the async methods (blocking forever since they're not properly awaited). Furthermore, as I was testing I discovered that TearDown methods were never executed in the old setup as the after-test commands were called after ExecuteTestMethod had completed (meaning the _afterTest list was always empty when called).

What is the updated/expected behavior with this PR?

SetUp and TearDown do not block threads and TearDown actually runs.

How was the solution implemented (if it's not obvious)?

We now reflect into the before/after commands and obtain the list of setup methods (assuming the command target is of type SetUpTearDownItem). We then get the list of setup methods and then add them to the _beforeTest list, appropriately awaiting the ones that return Task or ValueTask (same behavior as currently exists for tests). If the command target is not of type SetUpTearDownItem, we execute the old behavior. For after tasks, we do the same thing except rather than adding to the _afterTest list, we simply have the command execute the after task directly. The after parameters and _afterList fields have been removed we add the Actions with test context directly to a list of Action<TestExecutionContext> and then execute the tests inside the ExecuteTestMethod as before.

Checklist

Breaking changes

None.

Obsoletions / Deprecations

None.

Fixed issues

Fixes #18304

@jonko0493
Copy link
Contributor Author

Those test failures are not a good sign, but it's also 2 am. I will look into them shortly!

@jonko0493
Copy link
Contributor Author

If you set a breakpoint in the debugger at this line under the NUnit tests, the breakpoint does not get hit when on master. It looks like my changes definitely break some other things, in part because I didn't fully consider the consequences of removing the TearDown tests from the custom test context. I'm rethinking how I did this.

@jonko0493
Copy link
Contributor Author

Looks like the tests that were failing before are passing now that I rethought how I did this!

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.3.999-cibuild0055123-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.3.999-cibuild0055125-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.3.999-cibuild0055129-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

{
Action<TestExecutionContext> beforeAction = c =>
{
before.AddRange(setUpMethods.Select<IMethodInfo, Action>(m => async void () =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async void won't work correctly: only part of the async methods (until the first await) will run as expected, with the continuation running out of order (possibly even after the test), or not at all.

The before and after list should probably have items of type Func<Task>, with awaiting taking place in ExecuteTestMethod.

before.AddRange(setUpMethods.Select<IMethodInfo, Action>(m => async void () =>
{
var result = m.Invoke(c.TestObject, []);
if (result is Task task)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic to await Task/ValueTask is now repeated 3 times in this class. Extracting it to an helper method would help keeping the code clean.

Action<TestExecutionContext> beforeAction = c => before.Add(() => beforeTest(c));
s_beforeTest.SetValue(beforeAndAfterTestCommand, beforeAction);
var setUpTearDownInfo = beforeTest.Target?.GetType()
.GetField("setUpTearDown", BindingFlags.Instance | BindingFlags.Public);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how I feel about this. Reflection is always a last resort, but at least before this PR it was only protected fields, part of the exposed API.

After this PR, reflection is used on a compiler-generated closure type, with a field matching a variable name... This seems brittle, but I don't think we have much of a choice.

For example, this won't work in NUnit 4 (which we don't officially support yet in Avalonia.Headless), since the code has changed: https://github.com/nunit/nunit/blob/main/src/NUnitFramework/framework/Internal/Commands/SetUpTearDownCommand.cs

cc @maxkatz6

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I agree completely, this was just the best solution I could come up with to ensure these methods run. To be clear, there are workarounds users can implement; because asynchronous test execution works, one can forego the [SetUp]/[TearDown] attributes and manually invoke the methods from each test that needs them (e.g. what we do here). This is not ideal, but if documented could certainly be adequate if this solution is determined to be too brittle.

The ideal solution is of course an NUnit change to make the test execution environment more extensible. Hopefully the issue you commented on can get prioritized to allow for that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does NUnit 4 has better extensibility support? If it does, we can completely drop 3.x support together with reflection.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, it seems it does not

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Avalonia.Headless.NUnit] async SetUp/TearDown methods block tests forever
4 participants