Test breaks when using Future.delayed

Viewed 8432

Fade in FAB

I have used a Future.delayed to show a FAB in 1 second using this code:

Future.delayed(const Duration(seconds: 1), () {
  setState(() {
    _showFab = true;
  });
});

Now the most basic smoke test has stopped working:

void main() {
  testWidgets('smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());

    expect(find.byType(MyHomePage), findsOneWidget);
  });
}

This is the error message:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
A Timer is still pending even after the widget tree was disposed.
'package:flutter_test/src/binding.dart': Failed assertion: line 933 pos 7:
'_currentFakeAsync.nonPeriodicTimerCount == 0'    import 'dart:async';

Here is all the code used:

import 'package:flutter/material.dart';
import 'dart:async';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _showFab = false;

  @override
  Widget build(BuildContext context) {
    Future.delayed(const Duration(seconds: 1), () {
      setState(() {
        _showFab = true;
      });
    });

    return Scaffold(
      floatingActionButton: AnimatedOpacity(
        opacity: _showFab ? 1.0 : 0.0,
        duration: Duration(milliseconds: 1400),
        child: FloatingActionButton(
          onPressed: null,
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

How can I change the unit test to make the test pass?

5 Answers

The test framework runs with FakeAsync by default, that has some limitations and might cause the error you get.

You can explicitly run with async proper like:

void main() {
  testWidgets('smoke test', (WidgetTester tester) async {
    await tester.runAsync(() async {
      await tester.pumpWidget(MyApp());

      expect(find.byType(MyHomePage), findsOneWidget);
    });
  });
}

You might still need suggestions made in other answers.

You need to give the tester enough time to process all the async calls you have scheduled, try change your smoke test to something like this:

void main() {
    testWidgets('smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());
    // Since you wait 1 second to start the animation and another 1.4 to complete it
    await tester.pump(Duration(seconds: 3));

    expect(find.byType(MyHomePage), findsOneWidget);
  });
}

You will also need to move the Future.delayed out of the build() method, because this is causing a cyclic behaviour, every time you call setState() the build() is called again, change your state like that:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _showFab = false;

  @override
  void initState() {
    Future.delayed(const Duration(seconds: 1), () {
      setState(() {
        _showFab = true;
      });
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: AnimatedOpacity(
        opacity: _showFab ? 1.0 : 0.0,
        duration: Duration(milliseconds: 1400),
        child: FloatingActionButton(
          onPressed: null,
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

You can wrap your Widget with TickerMode.

As you can see below:

void main() {
    testWidgets('smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(TickerMode(child: MyApp(), enabled: false));
    await tester.pumpAndSettle();
    expect(find.byType(MyHomePage), findsOneWidget);
  });
}

Try calling pumpAndSettle after you've pumped the widget. Like this:

void main() {
  testWidgets('smoke test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());
    await tester.pumpAndSettle();

    expect(find.byType(MyHomePage), findsOneWidget);
  });
}

From the docs

This essentially waits for all animations to have completed.

I tried using pump and pumpAndSettle but it didn't work. But add Duration in pumpAndSettle (not work for pump) makes it work again

await tester.pumpAndSettle(Duration(seconds: 1));
Related