Quick / Nimble

A Matcher Framework for Swift and Objective-C
https://quick.github.io/Nimble/documentation/nimble/
Apache License 2.0
4.81k stars 598 forks source link

How do I expect notifications correctly in an async context? #1012

Closed abushnaq-work closed 1 year ago

abushnaq-work commented 1 year ago

What did you do?

Added code trying to test for a notification being fired in an async context:

let notification = Notification(name: Constants.BadJSONNotification) expect { await expect(await provisioningManager.processJSON(badJSON).to(beFalse()) }.toEventually(postNotifications(equal([notification])))

What did you expect to happen?

Initially expected it to work. I got the warning: Instance method 'toEventually' is unavailable from asynchronous contexts; the sync version oftoEventuallydoes not work in async contexts. Use the async version with the same name as a drop-in replacement; this is an error in Swift 6

and have so far completely failed to find the async version

What actually happened instead?

I have completely failed to either find an example, documentation, or the definition of the aforementioned async version.

Environment

List the software versions you're using:

Please also mention which package manager you used and its version. Delete the other package managers in this list:

Project that demonstrates the issue

Nothing to demonstrate just hopefully someone can point me to the right place if this is supported.

abushnaq-work commented 1 year ago

Did I maybe misunderstand and this https://github.com/Quick/Nimble/pull/1007/ is not in 11.1 but will be in a later version?

younata commented 1 year ago

My apologies for the poor wording. I struggled with how to correctly word that message.

Prepend await to the entire expression. For example: await expect(blah).toEventually(...).

abushnaq-work commented 1 year ago

Hi and thank you for the quick answer,

It seems to be required to run on the main thread? It's tripping an assertion on Nimble/PostNotification.swift:64: Assertion failed: Only expecting closure to be evaluated on main thread..

This is my first project with async/await so I'm a bit shaky. I tried surrounding that with MainActor.run to ensure it runs on the main thread:

MainActor.run
                        {
                            let notification = Notification(name: Constants.BadJSONNotification)
                            await expect {
                                await expect(await provisioningManager.processFeatures(emptyJSON)).to(beFalse())
                            }.toEventually(postNotifications(equal([notification])))
                        }

But that triggers a compile error Cannot pass function of type '@Sendable () async -> ()' to parameter expecting synchronous function type.

Part of me wants to tag everything with await up the chain, but that doesn't make sense as I earlier had await expect(await provisioningManager.processJSON(badJSON).to(beFalse()) and it worked fine without tagging more items up the chain as async.

younata commented 1 year ago

Oh. I'm sorry. I misunderstood what you were doing. This is confusing, and I'm sorry.

As of Nimble 11, toEventually does not work async Expressions. That is, expect { await someAsyncFunction() }.toEventually(...) is not supported and will not compile. In this case, Expression is the value or closure passed in to expect.

toEventually works using a polling mechanism - it runs the Expression (the value or closure passed in to expect) many times until the matcher passes. In Nimble 11, the way async Expressions are supported is a bit of a hack that I bolted on to Nimble's existing infrastructure; Nimble awaits the async Expression, then uses the resulting value with the existing, synchronous, infrastructure. This approach is fundamentally incompatible with toEventually's approach of re-running the Expression every pollInterval: all that would happen is it would test the same value it received previously.

I would like to fix this, but I've yet to come up with a good solution for all of the problems introduced by it.

Again, I'm really sorry for this confusion.

abushnaq-work commented 1 year ago

No worries I appreciated the detailed and patient explanation. So there's currently no way to test an async function for notifications then - is that right?

Maybe I can look into XCTest for that part and have Nimble do everything else. I appreciate all the work done in Nimble - this is my second project with it and it just makes sense. :)

younata commented 1 year ago

So there's currently no way to test an async function for notifications then - is that right?

That's correct. You could certainly cobble together something yourself that does it, but Nimble doesn't provide anything out of the box for this.

I created https://github.com/Quick/Nimble/issues/1013 to specifically track this issue (and in general other wonkiness with the approach I took when implementing Async support for expectations).

abushnaq-work commented 1 year ago

Got it, thanks for your help.