simolus3 / drift

Drift is an easy to use, reactive, typesafe persistence library for Dart & Flutter.
https://drift.simonbinder.eu/
MIT License
2.66k stars 372 forks source link

Pending timers in tests using streams #3323

Closed GP4cK closed 2 weeks ago

GP4cK commented 2 weeks ago

If I create a widget test that pumps a widget that is using a watch query, the test will fail with this error:

Pending timers:
Timer (duration: 0:00:00.000000, periodic: false), created:
#0      new FakeTimer._ (package:fake_async/fake_async.dart:308:62)
#1      FakeAsync._createTimer (package:fake_async/fake_async.dart:252:27)
#2      FakeAsync.run.<anonymous closure> (package:fake_async/fake_async.dart:185:19)
#6      StreamQueryStore.markAsClosed (package:drift/src/runtime/executor/stream_queries.dart:137:11)
#7      QueryStream._onCancelOrPause (package:drift/src/runtime/executor/stream_queries.dart:288:14)
#8      QueryStream._stream.<anonymous closure>.<anonymous closure> (package:drift/src/runtime/executor/stream_queries.dart:220:11)
// main.dart
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  final db = AppDatabase();
  runApp(MyApp(db));
}

class MyApp extends StatefulWidget {
  const MyApp(this.db, {super.key});
  final AppDatabase db;

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final stream = widget.db.select(widget.db.todoItems).watch();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        home: Scaffold(
          body: StreamBuilder(
            stream: stream,
            builder: (context, snapshot) {
              final items = snapshot.data ?? const [];
              return ListView(
                children: items
                    .map((item) => ListTile(title: Text(item.title)))
                    .toList(),
              );
            },
          ),
        ));
  }
}

// test
void main() {
  late AppDatabase db;
  setUpAll(() {
    db = AppDatabase();
  });
  tearDownAll(() {
    db.close();
  });
  testWidgets('Will fail', (tester) async {
    await tester.pumpWidget(MyApp(db));
  });
}

I've created a sample repo here: https://github.com/GP4cK/drift_stream_timer_issue

As a workaround, if I wrap the test with a runAsync() and pump a Container at the end, then it passes:

testWidgets('Workaround', (tester) async {
  await tester.runAsync(() async {
    await tester.pumpWidget(MyApp(db));
    await tester.pumpWidget(Container());
  });
});
simolus3 commented 2 weeks ago

Thanks for the report! This is a known issue, at the moment you have to close databases before the test finishes to clear up the outstanding timers. Drift uses these timers to not immediately invalidate stream queries after a listener detaches (which can improve performance around common patterns like StreamBuilders that detach and immediately re-attach on rebuilds).

I think we should have an option to disable that behavior because this makes drift a lot harder to use in Flutter unit tests, and this optimization is not really necessary there.

GP4cK commented 2 weeks ago

That was fast! Looking forward to the next release. Thank you!