Code Monkey home page Code Monkey logo

fake_async's Introduction

Dart CI pub package package publisher

This package provides a FakeAsync class, which makes it easy to deterministically test code that uses asynchronous features like Futures, Streams, Timers, and microtasks. It creates an environment in which the user can explicitly control Dart's notion of the "current time". When the time is advanced, FakeAsync fires all asynchronous events that are scheduled for that time period without actually needing the test to wait for real time to elapse.

For example:

import 'dart:async';

import 'package:fake_async/fake_async.dart';
import 'package:test/test.dart';

void main() {
  test("Future.timeout() throws an error once the timeout is up", () {
    // Any code run within [fakeAsync] is run within the context of the
    // [FakeAsync] object passed to the callback.
    fakeAsync((async) {
      // All asynchronous features that rely on timing are automatically
      // controlled by [fakeAsync].
      expect(Completer().future.timeout(Duration(seconds: 5)),
          throwsA(isA<TimeoutException>()));

      // This will cause the timeout above to fire immediately, without waiting
      // 5 seconds of real time.
      async.elapse(Duration(seconds: 5));
    });
  });
}

Integration With clock

FakeAsync can't control the time reported by DateTime.now() or by the Stopwatch class, since they're not part of dart:async. However, if you create them using the clock package's clock.now() or clock.stopwatch() functions, FakeAsync will automatically override them to use the same notion of time as dart:async classes.

fake_async's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fake_async's Issues

Exceptions from async callbacks to FakeAsync are swallowed

Running the following prints no diagnostic:

void main() async {
  await FakeAsync().run<Future<void>>((_) async {
    throw Exception('exception');
  });
}

This seems surprising, and if it's not expected to work, it doesn't seem obvious from the FakeAsync documentation.

This also seems like a footgun, especially since FakeAsync is likely to be used in a test() callback, where expect() calls can appear to pass when they actually fail.

Clock.now() returns the same objects as FakeAsync's DateTime

In my service under test, I have

this.sessiontokenExiry = clock.now().add(Duration(minutes: 3)); // clock from clock package

And in my unit test, I run

async.elapse(new Duration(seconds: 1));
Somehow, this.sessiontokenExiry == clock.now() // true

So async.elapse mutates this.sessiontokenExiry which is not intended. It is expected that clock.now() always return a new object.

Why does making the callback function async break expect?

I think I just don't understand this, but why does this test fail (as expected):

test('test smoke test -- this test should fail', () {
  fakeAsync((async) {
    expect(true, isFalse);
  });
});

But this one passes:

test('test smoke test -- this test should fail', () async {
  fakeAsync((async) async {
    expect(true, isFalse);
  });
});

I don't understand this behavior, and I think it is the root of several bugs within my tests

Make the `creationStackTrace` getter nullable in `FakeTimer`

#40 will make the getter throw in some cases. This is an OK compromise since a major version bump has a high cost here, and most folks won't run in to this behavior. If we were designing the API without historical constraints, we'd probably make it nullable.

Change of Copyright notice

Hello! According to the Appendix of Apache License 2.0, if you want to license your software under this License you should "attach the boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information". This condition is not met now. Please add a copyright notice in the appropriate form, including the year of the software development and your name and surname. Thank you in advance

Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Use fakeAsync with asynchronous generator (yield*)

Hi!
I'm having trouble with using fakeAsync with code that depends on asynchronous generator. Possibly it's something that I don't understand about async*/yield* that causes this, but I'd like to know what's happening, and if perhaps fakeAsync can work with code depending on it.

See https://gist.github.com/kpsroka/c6f58ef942fb413883942baff2f4de57 for an example.

Edit: I've updated the example with several other cases where fakeAsync does not do the same work as in the straightforward case, which also hints and my lack of understanding of limitations of the tool.

fakeAsync seems to not work with Directory.create

I tried to implement a test for one a Stream with the help of fakeAsync, but I got stuck, since the method under test never finishes, because it seems to never get pass an awaited Directory.create() call.

Example with two test cases, the one that fails and a modified one (with the only difference being that the Directory.create() call is commented out.

import 'dart:async';
import 'dart:io';

import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('fakeAsync bad', () {
    fakeAsync((async) {
      var controller = StreamController<int>();
      controller.onListen = () async {
        print('onListen');
        await Future.delayed(Duration(milliseconds: 500));
        print('step 1');
        await Directory.systemTemp.createTemp();
        print('step 2');
        controller.add(1);
        await controller.close();
        print('onListen done');
      };

      var log = <int>[];
      controller.stream.listen(log.add);
      do {
        async.flushMicrotasks();
        async.elapse(Duration(milliseconds: 500));
      } while (async.microtaskCount != 0 || async.pendingTimers.isNotEmpty);
      expect(log, [1]);
    });
  });

  test('fakeAsync good', () {
    fakeAsync((async) {
      var controller = StreamController<int>();
      controller.onListen = () async {
        print('onListen');
        await Future.delayed(Duration(milliseconds: 500));
        print('step 1');
//        await Directory.systemTemp.createTemp();
        print('step 2');
        controller.add(1);
        await controller.close();
        print('onListen done');
      };

      var log = <int>[];
      controller.stream.listen(log.add);
      do {
        async.flushMicrotasks();
        async.elapse(Duration(milliseconds: 500));
      } while (async.microtaskCount != 0 || async.pendingTimers.isNotEmpty);
      expect(log, [1]);
    });
  });
}

In the bad test case, the last message that is printed is step1 and I cannot get the test to actually pass. In my real code, the stream comes from a function call, so I cannot really change it.

How can I change the test to pass without touching the onListen implementation?

Varying the precision of Duration

See this discussion here : dart-lang/sdk#54769 (comment).

I'd like to vary the precision of Duration so that we have a finer grained precision - say down to picoseconds. This is for modeling hardware systems in Dart.

It seems to me that under the hood, Duration is basically an int. Currently that int is measured in microseconds.

Would it be possible to generalise the notion of time as used by fakeAsync to use a more generic notion of a Duration ?

The thread above suggested forking the entire repo fakeAsync repo, but I'd prefer to avoid that if possible.

Add some way to go into a "normal" execution mode, and then back into being controlled by fake async

Sometimes it's nice to be able to have a little block of async/await code in your test. await is very tricky to use in a fake async zone since it will never complete until either elapse or flushMicrotasks is called, but you can't get past it until then.

It might be possible to add some way that at least the event loop and microtask queue still operate normally. Making timers fire would likely not be possible, but also doesn't impact the await case.

Proposal: Do not mock microtasks by default

I think that while the ability to mock microtasks can be useful, most use-cases don't really care about this. Instead, people likely use fake_async for Duration based APIs like clocks or timers

By not mocking microtasks, this could allow writing things like:

await fakeAsync((async) async {
  // random example showing how we can mix elapse and async/await
  final repository = Repository(cacheTime: Duration(seconds: 5));
  repository.cache = {'value': 42};

  final response = await fetchSomething(repository); // would otherwise block if microtasks were mocked
  expect(response.value, 42);

  async.elapse(Duration(seconds: 5));

  expect(repository.cache, null);
});

Unless I'm missing something important and mocking microtasks is necessary for proper Timer mock, I think this behavior would be more intuitive for users.

FakeAsync hangs when operating on a Stream

This test:

        test('Should interrupt on timeout', () {
            //given
            var controller = StreamController<Uint8List>();
            const expectedError = 'No response';

            //when
            result() {
              final comleter = Completer<bool>();
              controller.stream.timeout(
                Duration(seconds: 5),
                onTimeout: (sink) {
                  print('on timeout!');
                  sink.close();
                },
              ).listen((event) {
              }, onDone: () {
                print('onDone');
                comleter.completeError(Exception(expectedError));
              });
              return comleter.future;
            }

            //then
            expect(result, throwsA(predicate((e) => e is Exception && e.toString() == 'Exception: $expectedError')));
        });

passes as expected after 5 seconds.
But when I apply FakeAsync:

        test('Should interrupt on timeout', () {
          fakeAsync((async) {
            //given
            var controller = StreamController<Uint8List>();
            const expectedError = 'No response';

            //when
            result() {
              final comleter = Completer<bool>();
              controller.stream.timeout(
                Duration(seconds: 5),
                onTimeout: (sink) {
                  print('on timeout!');
                  sink.close();
                },
              ).listen((event) {
              }, onDone: () {
                print('onDone');
                comleter.completeError(Exception(expectedError));
              });
              return comleter.future;
            }

            //then
            expect(result, throwsA(predicate((e) => e is Exception && e.toString() == 'Exception: $expectedError')));

            async.elapse(Duration(seconds: 6));
          });
        });

it displays:

on timeout!
onDone

as expeceted, but then test hangs and is interrupted after 30sec by timeout:

00:31 +0 -1: Should interrupt on timeout [E]
  TimeoutException after 0:00:30.000000: Test timed out after 30 seconds. See https://pub.dev/packages/test#timeouts
  dart:isolate  _RawReceivePort._handleMessage

_FakeTimer doesn't have a `tick` member

As reported in dart-archive/http_retry#6, http_retry tests are now failing with this error:

> pub run test --platform vm
00:02 +0 -1: loading test/http_retry_test.dart [E]                                                                                                                                                     
  Failed to load "test/http_retry_test.dart":
  Unable to spawn isolate: file:///home/travis/.pub-cache/hosted/pub.dartlang.org/fake_async-0.1.2/lib/fake_async.dart:212:7: Error: The non-abstract class '_FakeTimer' is missing implementations for these members:
    'tick'.
  Try to either
   - provide an implementation,
   - inherit an implementation from a superclass or mixin,
   - mark the class as abstract, or
   - provide a 'noSuchMethod' implementation.
  
  class _FakeTimer implements Timer {
        ^^^^^^^^^^
  file:///home/travis/.pub-cache/hosted/pub.dartlang.org/fake_async-0.1.2/lib/fake_async.dart:247:61: Error: The static method has type '(dart.core::Comparable<dynamic>, dart.core::Comparable<dynamic>) → dart.core::int' that isn't of expected type '(dynamic, dynamic) → dart.core::int'.
  Change the type of the method or the context in which it is used.
  dynamic _minOf(Iterable i, [Comparator compare = Comparable.compare]) =>
                                                              ^
00:02 +0 -1: Some tests failed.  

See https://travis-ci.org/dart-lang/http_retry/builds/404507054

cc @kevmoo

How to call "await" within "fakeAsync"

Hello,

how can I await a Future within a fakeAsync zone?

When I do this, than the callback doesn't get executed:

    fakeAsync((fakeAsync) async {
          await someAsyncMethod();
    });

When I await the fakeAsync, it never returns.

    await fakeAsync((fakeAsync) async {
          await someAsyncMethod();
    });

Any idea?

Best,

Niklas

(with minimal reproducible sample) `Stream.toList` stuck forever in a fake_async environment

void main() {
  test('simple', () async {
    await fakeAsync((async) async {
      final stream = Stream.value(42);
      print('hi before await');
      await stream.toList();
      print('hi after await');
    });
  });
}

stucks forever...

I have also tried to call whatever methods, but still stuck forever in the "await" line

  test('simple', () async {
    await fakeAsync((async) async {
      final stream = Stream.value(42);
      final future = stream.toList();
      print('hi before await');
      async.flushMicrotasks();
      async.flushTimers();
      async.elapse(const Duration(seconds: 10));
      async.elapseBlocking(const Duration(seconds: 10));
      await future;
      print('hi after await');
    });
  });

I have looked at the implementation of Stream.toList as follows, but it seems quite normal

  Future<List<T>> toList() {
    List<T> result = <T>[];
    _Future<List<T>> future = new _Future<List<T>>();
    this.listen(
        (T data) {
          result.add(data);
        },
        onError: future._completeError,
        onDone: () {
          future._complete(result);
        },
        cancelOnError: true);
    return future;
  }

stream.firstOrNull with timeout confusing behavior

I have stumbled across a difference in behavior between stream.first and stream.firstOrNull when using timeouts with fakeAsync:

  // Passes as expected
  test('stream.first with timeout', () async {
    fakeAsync((FakeAsync async) {
      expect(
        Stream<int>.value(0).first.timeout(const Duration(seconds: 1)),
        completion(0),
      );
      async.elapse(const Duration(seconds: 10));
    });
  });

  // Fails with timeout exception
  test('stream.firstOrNull with timeout', () async {
    fakeAsync((FakeAsync async) {
      expect(
        Stream<int>.value(0).firstOrNull.timeout(const Duration(seconds: 1)),
        completion(0),
      );
      async.elapse(const Duration(seconds: 10));
    });
  });

I would expect these tests to behave identically given that both streams are guaranteed to have a value. Could this be the intended behavior? My main concern is that fakeAsync is sensitive to the implementation details of methods that otherwise behave identically (in this case because the stream has a value). This would make refactoring tested code a real pain. But maybe I am missing something.

I only ran into this issue while writing tests with fakeAsync. The actual code being tested, which uses firstOrNull, seems to work fine while debugging, but the tests fail because of the timeout.

As a workaround, I am using a custom firstOrNull with a slightly different implementation that makes it work like I would expect:

extension FirstOrNull2Extension<T> on Stream<T> {
  Future<T?> get firstOrNull2 async {
    final Completer<T?> completer = Completer<T?>.sync();
    final StreamSubscription<T> subscription = listen(
      completer.complete,
      onError: completer.completeError,
      onDone: completer.complete,
      cancelOnError: true,
    );
    return completer.future.whenComplete(subscription.cancel);
  }
}

(Not sure if this mimics firstOrNull 100%, but dropping it in my code fixed my issues)

My first attempt at an alternate firstOrNull implementation gave me the same problem when applying timeouts in a fakeAsync:

extension FirstOrNull2Extension<T> on Stream<T> {
  Future<T?> get firstOrNull2 async {
    await for (final T event in this) {
      return event;
    }
    return null;
   }
}

Dart 2 runtime failures in tests

00:03 +8 -1: FakeAsync elapse should throw when called before previous call is complete [E]
type '(dynamic, dynamic) => dynamic' is not a subtype of type '(_FakeTimer, _FakeTimer) => _FakeTimer' of 'combine'
dart:core Iterable.reduce
package:fake_async/fake_async.dart 236:26 _minOf
package:fake_async/fake_async.dart 166:12 _FakeAsync._getNextTimer
package:fake_async/fake_async.dart 107:20 _FakeAsync.elapse
../../fake_async/test/fake_async_test.dart 96:17 main....

00:03 +8 -2: FakeAsync elapse when creating timers should call timers expiring before or at end time [E]
type '(dynamic, dynamic) => dynamic' is not a subtype of type '(_FakeTimer, _FakeTimer) => _FakeTimer' of 'combine'
dart:core Iterable.reduce
package:fake_async/fake_async.dart 236:26 _minOf
package:fake_async/fake_async.dart 166:12 _FakeAsync._getNextTimer
package:fake_async/fake_async.dart 107:20 _FakeAsync.elapse
../../fake_async/test/fake_async_test.dart 112:19 main.....

and so on....

StreamSubscription.cancel hangs up

I have a test that passes normally if run without FakeAsync, but freezes mid execution under FakeAsync.
After some time debugging, it looks like the problem is somehow related to StreamSubscription's cancel method.

Here is a sample to demonstrate:

import 'dart:async';

import 'package:fake_async/fake_async.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';

class ServiceUnderTest {
  ServiceUnderTest(this.stream);

  Stream<bool> stream;
  StreamSubscription<dynamic>? _subscription;

  Future<void> start() async {
    _subscription = stream.listen((event) {
      debugPrintSynchronously('event: $event');
    });
  }

  Future<void> stop() async {
    debugPrintSynchronously('stop begun');
    await _subscription?.cancel();
    _subscription = null;
    debugPrintSynchronously('stop 1');
    await _subscription?.cancel();
    debugPrintSynchronously('stop finished');
  }
}

void main() {
  late ServiceUnderTest serviceUnderTest;

  setUp(() {
    final controller = StreamController<bool>();
    controller.add(true);

    serviceUnderTest = ServiceUnderTest(controller.stream);
  });

  test('No fakeAsync - Passes normally', () async {
    await serviceUnderTest.start();
    await expectLater(serviceUnderTest.stop(), completes);
  });

  test('With fakeAsync - Hangs up', () async {
    FakeAsync().run((async) {
      serviceUnderTest.start();
      async.flushMicrotasks();
      async.elapse(const Duration(seconds: 2));

      expectLater(serviceUnderTest.stop(), completes);
      async.flushMicrotasks();
      async.elapse(const Duration(seconds: 2));
    });
  });
}

First test would pass with no problems, but second one will output:

event: true
stop begun
stop 1

and hang up.

Is this an expected behaviour ? If so, how could i remedy it.

Prevent accidental misuse of fake_async when using async/await

I use fakeAsync for some test cases in my daily work.
I have accidentally written a test case which looks like this:

FakeAsync().run((fakeAsync) async {
  functionWithTimer();
  fakeAsync.elapse(t);
  final x =  await otherFunction();
  expect(x, expectedX);
});

I only realized a few weeks later (after the code went through a review by multiple experienced engineers and it was submitted), that the expectation doesn't actually run. I could add a throw Exception() after the expect call, and my test case would still succeed. This is because asynchronous callbacks passed to FakeAsync.run are effectively not awaited.

Unfortunately I was not the only one making this mistake, but I have seen this in multiple code files.

I'm wondering whether this kind of errors could be prevented.

Here is a naive idea:

  • We could disallow callbacks that return a Future in FakeAsync.run(). (Or allow it, but only with a special bool parameter for opt-in.)
  • We could provide another similar function, that makes sure that the callback's result is awaited and fully runs. It could use a combination of awaits and flushTimers or flushMicrotasks calls to achieve this.

Please feel free to contact me if you need an example for the erroneous use-case or the possible solution.

Single-step version of flushTimers

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 #24, #38, and #51.

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.

Provide guidance about package:fake_async vs. FakeAsync from quiver

It's confusing that both package:fake_async and package:quiver both provide mostly-similar-but-not-the-same FakeAsync classes and that both are owned by the Dart team. I tried digging into the history, but it seems rather muddled (it looks like it was added to quiver first, and then a few months later the creator forked it into a separate package without clear explanation why). And while package:fake_async at some point was backwards compatible with quiver's, there's been some divergence.

Should package:fake_async guide people to use the quiver version instead (since that one seems to be maintained, is what Flutter uses, and has more inertia)?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.