-
Notifications
You must be signed in to change notification settings - Fork 746
Replacing ThrowsAsync with a composable async alternative #2843
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
Comments
I would like to get more @nunit/framework-and-engine-team members to weigh in here too. I have always found it odd that we do sync-over-async and don't require people to await async asserts. The original code was written before I joined the team and I think much of it was inherited from 2.6. I always thought that our async asserts would be much simpler if they were async all the way down. I'm not sure, but I think that the original decision might have been to support Backing out of this mistake is a thorny issue though. We don't want to break existing code. Even deprecation warnings are a pain. Modern C# code has That said, I like the I would appreciate if other contributors and the NUnit community as a whole could weigh in on this. I think it is a great idea in principle, but I would like to do it in a way that will cause the least pain for people. |
@rprouse I think our attitude toward asserts has been formed by our attitude toward async tests. In general, we have to run tests synchronously, even if they are async, in order to be able to capture the result. We applied the same principle to asserts, I think with good reason. Our contract has always been that execution terminates when an assertion fails. Therefore, we had to wait for the assertion to complete in order to know if we should continue or not. Of course, we now have multiple asserts and warnings, so execution doesn't always terminate on failure. I think we probably should think about true async assertions, but I don't think it can be done cleanly in the current architecture. We would have to put as much thought and design work into it as we did into parallel execution of tests. |
AsyncAssert is a really good suggestion. I have an idea for what it would look like. If I put up a spike, it might be something to talk about. We would still run the entry points sync-over-async. Test methods, setups, etc. We'd just provide a new true-async assertion API. The framework architecture stays the same but we gain a new library API. What I'm thinking of might in theory even be able to be done as an outside library extending NUnit. |
We could also consider not providing a deprecation warning and shipping an analyzer with a suggestion, or even leave it to XML docs and release notes. @JohanLarsson Have you had experience writing a fix-all to update API usage, changing |
Just to be clear: I don't think it would be a bad idea to make the framework's worker infrastructure true-async, but I'm not sure what we'd gain and I don't want to jeopardize our standing in Johan's benchmarks. 😊 |
@jnm2 Yes, fix all is a bit of a pain using using the built in stuff in Roslyn but there are ways to make it work. I'm mixed about async asserts as they add some noise to tests. |
@JohanLarsson just curious, are your images for Rider benchmarks swapped for NUnit and xUnit? It seems odd that they would be opposite on each IDE. |
@rprouse I did not run the Rider benchmarks, was contributed in a PR, so not sure. |
Ran into a deadlock today in a test with a custom synchronization context because |
I started exploring this in https://github.com/nunit/nunit/compare/jnm2/async_assert. Many of the things I want to do will be easier once I finish my work on non-Task awaitables. An early question: what is our policy on adding methods to public interfaces, such as If we instead introduce |
EDIT: I think I've got it - sorry! Even though My reason for asking - as a member of the NUnit community - also willing to get involved and create PRs if it will help: It's noticeably more fiddly to write |
That sounds good for a v4. @rprouse, what do you think about doing something sooner than v4 like |
Because of Even I think we should do |
Is this causing pain to enough people to make big changes now? I expect that if we change the behaviour of I think we're in a bit of a corner here and I don't know what the right answer is. We can't change So, I'm okay with introducing true async asserts with new I would like to see analyzers gentle pushing people to better alternatives. |
It's quite easy to create analyzer with warning (or error) when Simple code fix to use |
There are similiar problems with For Multiple maybe we can have a simple solution with `Assert.MultipleAsync' which returns Task, but for Throw that is not a good option. |
I just saw a PR with a proposed fix #3577 |
So... We should have async methods anyway. |
I think In order to support |
I just ran into this issue Assert.ThrowsAsync should return a Task so it is awaitable. |
I'm sad that I'm currently hitting a deadlock due to #2917 not posting to a custom synchronization context, which could be avoided if it did just let the await go all the way down. |
@jnm2 @Daniel-Svensson @uecasm @trampster @Dreamescaper @mikebeaton @JohanLarsson Can you guys confirm that https://www.myget.org/feed/nunit/package/nuget/NUnit/4.0.0-dev-07733 is working for you? |
Hi - thanks! I am using NUnit3TestAdapter with Visual Studio - I cannot immediately see an NUnit4TestAdapter to use for this test - should there be? |
@mikebeaton The NUnit3TestAdapter works with NUnit version 4. We have a bit of name and version mangling here. If we could have changed the name it would be without the number 3 there, it doesn't mean anything anymore. And, if you guys find something that says it doesn't anymore, we need to fix that. |
Okay, with NUnit3TestAdapter 4.5.0-alpha.4 from NuGet.org my existing tests are running as is with NUnit 4.0.0-dev-07798. Btw the version of NUnit3TestAdapter which I can find in the same feed (https://www.myget.org/F/nunit/api/v3/index.json) which contains NUnit 4.0.0-dev-07798 is NUnit3TestAdapter 4.4.0-dev-02338, which does not work with NUnit 4.0.0-dev-07798. The description of NUnit3TestAdapter 4.5.0-alpha.4 still mentions NUnit 3: On to the actual issue(!): I gather that even though there has been a major version change 3 to 4, the decision was eventually taken not been to make a breaking change to EDIT: Ré point a), see #2843 (comment) above. :-) |
I am hoping to replace:
I got as far as:
That works, but doesn't include an equivalent of the last part of my test (i.e. I'm not sure how to access the actual thrown exception, if I want to do more tests on it). It also seems that it might be much nicer to have a typed variant of |
I wouldn't call it a "decision" as such; I'm just a random mook and this was my first nunit PR 😁 There aren't currently any docs as such other than the test cases in the PR itself, but it's pretty self-explanatory.
You can check exception messages with Also, |
Ah, I'm glad there is So I'm now at:
Which is indeed equivalent to my original test. Might the existence of Also, how do I check an arbitrary property of the thrown exception (not just Thanks. |
https://docs.nunit.org/articles/nunit/writing-tests/constraints/ThrowsConstraint.html It supports most of the constraint expression syntax, so you can check other arbitrary properties by using If you find yourself writing the same complex constraints repeatedly, you can make your own custom constraint to shorten the syntax. You're not really supposed to need to directly obtain the result value or exception with the constraint-based asserts; only test them via the constraints. But if you really want to recover the exception object directly, you can just put the method call into a try-catch and not use |
@uecasm - okay, thanks for that. This clearly goes outside your contribution, assuming those are all pre-existing bits of NUnit that I just haven't used, but is there no .Where or .All (EDIT: or maybe I really mean .Select or .SelectThat) or similar which can be chained to apply an arbitrary lambda expression to (OR: to select a part of?) the exception which was thrown? |
@OsirisTerje - The summary is, you can definitely change things around to get truly async, but otherwise like-for-like tests, for the (admittedly fairly simple) things I was currently doing with If I can offer my opinion, it is that |
You can use Using autocomplete in your IDE of choice is usually the best way to discover the available constraint expressions. Not all of them appear to be documented on the website, but they do have autocomplete docs. |
Yes, I do know that. It was autocomplete that made me think there wasn't an InstanceOf<T> - user error, I guess. And knowing that Matches is the right thing to use is slightly more than just finding it in autocomplete, but thanks for the info. |
The constraints themselves are pretty well documented but the static methods used to instantiate them are unfortunately not. Remember you can always use |
Thank you. Not at computer, but I'd got as far as wondering whether there's something like:
Something like that should be possible with the strongly typed InstanceOf<T>, I guess? |
I already told you that one. The existing syntax is even shorter:
Or for |
There aren't any website docs for the |
No you misunderstand my question. You've told me the special case method for accessing |
Probably lost in one as docs were ported (2 or three times) to new platforms. Should be a documentation issue. |
Other than |
Today's Assert.ThrowsAsync and related methods do sync-over-async. This means they convert an async operation into a synchronous one. In other words, they block until the task is complete and return
void
or the return value directly, rather than returning an awaitable type themselves such asTask ThrowsAsync
orTask<Exception> CatchAsync
.What does the problem look like?
Why is this a problem?
Waiting synchronously for an async operation is a antipattern with real-world consequences. The consequences range from losing scalability (if you're doing
async
right, there is no thread) to outright deadlocks and other logic errors.Well, in 3.11, I fixed the deadlock issue if you instantiate Windows Forms or WPF controls in your tests. So what then, why not keep doing sync-over-async in
Assert
?First, deadlocks are still a threat. I only fixed deadlocks on recognized message-pumping SynchronizationContexts, and even that hasn't been stress-tested for reentry with assert delegates nested inside assert delegates. We shouldn't have to go to the length of testing sync-over-async-over-sync-over-async; our APIs shouldn't be allowing it like this.
Second, without a good reason, it goes in the face of hard-learned good practices with async/await. The async-await paradigm is complex enough for those who are still becoming familiar with it that we should not be requiring them to code their tests around this antipattern.
Assert.*Async
is doing nothing which justifies an exception. It's one thing for NUnit as a framework to do sync-over-async with the entry point, the test method itself—this is exactly like C# 7.1'sasync Task Main
—but it's quite another thing for NUnit as a library, inAssert.*Async
, to be doing sync-over-async as well.What does the solution look like?
I've had proposals for this nagging me for a year and a half now, and none of them seem like slam dunks.
If we just switch
Assert.ThrowsAsync
fromvoid
toasync Task
we silently break some people's code. And if we switchAssert.CatchAsync
fromException
toTask<Exception>
, we cause any code using it to stop compiling. The second instance might be considered good pain, but the first option (silent breakage) is absolutely unacceptable.➡ This means likely the best course is deprecation warnings for all
Assert.*Async
methods.(Assuming we don't ship a special analyzer in the NUnit package to turn the silent errors into compiler errors and provide a "fix all.")
If we do deprecation warnings, these are our options as I see them:
AssertAsync.Throws
andAssertAsync.That
where all the methods returnTask
orTask<>
.@rprouse I would like to fix this in 3.11 as part of the theme, if that seems good to you.
The text was updated successfully, but these errors were encountered: