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

Single-step version of flushTimers #2318

Open
gnprice opened this issue Jun 6, 2024 · 0 comments
Open

Single-step version of flushTimers #2318

gnprice opened this issue Jun 6, 2024 · 0 comments

Comments

@gnprice
Copy link
Contributor

gnprice commented Jun 6, 2024

I have some tests that I've written using this package, and initially I ran into the same kinds of pitfalls and sharp edges that have been described in a few previous issues in this tracker like #2303, #2307, and #2312.

Then after spending some time getting my head around how FakeAsync works, I developed a pattern for using it that I'm fairly happy with, with few enough sharp edges that I've been comfortable recommending it to coworkers without expecting them to spend a lot of time understanding the implementation like I did. There still are some sharp edges, though. In order to fix those, I think I need a bit more control over the loop that happens in flushTimers.

Specifically I'd like to have a FakeAsync method I can call that does much the same thing as flushTimers, but only goes one iteration through the loop, running one timer. Then I can write a loop that calls that method but also runs some logic of my own at each iteration, potentially deciding to break out of the loop.

I have a draft implementation of such a method, runNextTimer, which I'll send as a PR shortly (→#85). I wanted to file this issue to give the background on its motivation and my use case, and especially in case someone here has ideas for alternate solutions for the use case.


To make things concrete, the pattern I've found helpful is a helper function I call awaitFakeAsync. The docs say (cutting boring parts):

/// Run [callback] to completion in a [Zone] where all asynchrony is
/// controlled by an instance of [FakeAsync].
///
/// … After calling [callback], this function uses [FakeAsync.flushTimers] to
/// advance the computation started by [callback], and then expects the
/// [Future] that was returned by [callback] to have completed. …
T awaitFakeAsync<T>(Future<T> Function(FakeAsync async) callback,
    {DateTime? initialTime}) {

One uses it like so, more or less freely mixing plain await with calls to FakeAsync methods:

  test('the thing works', () {
    awaitFakeAsync((async) async {
      final thing = Thing();
      // Plain old await!  The [awaitFakeAsync] keeps it moving.
      await thing.start();
      check(thing.count).equals(0);

      await thing.schedule(Duration(seconds: 1));
      check(thing.count).equals(0);
      // But the test can also manipulate time via FakeAsync methods.
      async.elapse(Duration(seconds: 1));
      check(thing.count).equals(1);
    });
  });

Mostly this works great. The one main gotcha we've encountered is that if the callback's returned future completes with an error — i.e. when the test body throws an exception, when used in the above pattern — there's no way to break flushTimers out of its loop. As I write in a TODO comment in the awaitFakeAsync implementation:

  // TODO: if the future returned by [callback] completes with an error,
  //   it would be good to throw that error immediately rather than finish
  //   flushing timers.  (This probably requires [FakeAsync] to have a richer
  //   API, like a `fireNextTimer` that does one iteration of `flushTimers`.)
  //
  //   In particular, if flushing timers later causes an uncaught exception, the
  //   current behavior is that that uncaught exception gets printed first
  //   (while `flushTimers` is running), and then only later (after
  //   `flushTimers` has returned control to this function) do we throw the
  //   error that the [callback] future completed with.  That's confusing
  //   because it causes the exceptions to appear in test output in an order
  //   that's misleading about what actually happened.

I have a draft commit (gnprice/zulip-flutter@1ad0a6f) that converts that awaitFakeAsync helper to drive its own loop and call the FakeAsync.runNextTimer from my upcoming PR (→#85), and it solves that problem.

gnprice referenced this issue in zulip/dart-fake_async Jun 6, 2024
@mosuem mosuem transferred this issue from dart-archive/fake_async Oct 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants