How can I run a unit test when the tapped widget launches a Timer?

4,588

Solution 1

testWidgets automatically introduces a FakeAsync zone which lets you step through time. Use pump to advance time.

The error you're seeing, though, is because your widget isn't canceling the timer when it's disposed. Make sure that your Widget objects never allocate resources (like timers), and that your State objects always clean up any resources they allocate, in their dispose method.

Solution 2

Use await tester.pumpAndSettle(), by default it is using 100 milliseconds delay, but you can add you own value: await tester.pumpAndSettle(const Duration(seconds:1))

It can be useful if you test widget which uses Timer or some animated widget under the hood.

Here's a small example how to use it:

testWidgets('build with $state state', (tester) async {
        when(() => bloc.state).thenReturn(state);
        await blocTester.test(
          tester: tester,
          stateConfig: states[state]!,
        );
        await tester.pumpAndSettle();
      })

Solution 3

You should be able to do this inside a FakeAsync from Quiver's FakeAsync library: https://www.dartdocs.org/documentation/quiver/0.24.0/quiver.testing.async/FakeAsync-class.html

There are a number of examples of this pattern in the tests of the Flutter framework itself: https://github.com/flutter/flutter/search?utf8=%E2%9C%93&q=FakeAsync

Share:
4,588
Reagankm
Author by

Reagankm

Updated on December 02, 2022

Comments

  • Reagankm
    Reagankm over 1 year

    I have a Widget that, when clicked, saves an ID number, overlays a CircularProgressIndicator for 1000ms and then pops the progress indicator and routes the user to another page.

    This bit with the ProgressIndicator and the Timer is new and has broken my unit test, which now gives me the following error:

    The following assertion was thrown running a test:
    'package:flutter_test/src/binding.dart': Failed assertion: line 574 pos 12: '() {
          'A Timer is still pending even after the widget tree was disposed.';
          return _fakeAsync.nonPeriodicTimerCount == 0;
        }': is not true.
    
    Either the assertion indicates an error in the framework itself, or we should provide substantially
    more information in this error message to help you determine and fix the underlying cause.
    In either case, please report this assertion by filing a bug on GitHub:
      https://github.com/flutter/flutter/issues/new
    
    When the exception was thrown, this was the stack:
    #2      AutomatedTestWidgetsFlutterBinding._verifyInvariants (package:flutter_test/src/binding.dart:574:12)
    #3      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:415:7)
    <asynchronous suspension>
    #7      TestWidgetsFlutterBinding._runTest (package:flutter_test/src/binding.dart:392:14)
    #8      AutomatedTestWidgetsFlutterBinding.runTest.<anonymous closure> (package:flutter_test/src/binding.dart:549:24)
    #14     AutomatedTestWidgetsFlutterBinding.runTest (package:flutter_test/src/binding.dart:547:16)
    #15     testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:54:50)
    #16     Declarer.test.<anonymous closure>.<anonymous closure> (package:test/src/backend/declarer.dart:131:19)
    <asynchronous suspension>
    #17     Invoker.waitForOutstandingCallbacks.<anonymous closure>.<anonymous closure> (package:test/src/backend/invoker.dart:200:17)
    <asynchronous suspension>
    #22     Invoker.waitForOutstandingCallbacks.<anonymous closure> (package:test/src/backend/invoker.dart:197:7)
    #26     Invoker.waitForOutstandingCallbacks (package:test/src/backend/invoker.dart:196:5)
    #27     Declarer.test.<anonymous closure> (package:test/src/backend/declarer.dart:129:29)
    <asynchronous suspension>
    #28     Invoker._onRun.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:test/src/backend/invoker.dart:322:23)
    <asynchronous suspension>
    #43     _Timer._runTimers (dart:isolate-patch/timer_impl.dart:385)
    #44     _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:414)
    #45     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:148)
    (elided 31 frames from class _AssertionError, class _FakeAsync, package dart:async, package
    dart:async-patch, and package stack_trace)
    

    This is what the broken test looks like:

    testWidgets('Tapping item saves its id', (WidgetTester tester) async {
      await launchApp(item, tester);
      await tester.tap(find.byConfig(item));
    
      expect(sameId(global_state.currentId, item.id), isTrue);
    });
    

    Is there some way to introduce a delay before disposing of the widget tree? What are my other options? Or am I wrong about the root cause of the problem?

  • Philip
    Philip over 4 years
    Could you please give an example of resources that need to be cleaned up?