Testing Flutter callbacks

We all know that we should test our code. Flutter makes this really easy with all of testing frameworks that are coming bundled with it. You can write unit tests, widget tests and UI test.


Testing callbacks are particularly tiring. I bet you seen code like this many times, even more, if you are testing your widgets, I’m almost sure you have written this kind of code.

testWidgets('callback', (WidgetTester tester) async {
  var pressed = false;
  final onPressed = () => pressed = true;

  await tester.pumpWidget(
    MaterialApp(
      home: FlatButton(
        child: Text('press me'),
        onPressed: onPressed,
      ),
    ),
  );

  await tester.tap(find.byType(FlatButton));

  expect(pressed, isTrue);
});

This pattern is fairly common. Declaring a bool flag that will be changed inside of callback function. No magic over here…

But writing this much code only to test simple callback hurts my programmer soul… Could it be done better?


Sure one can write helper function for this… And also another set of tests for this helper function. But as we all know, the best code is one that we didn’t wrote our selves.

Let me introduce you to Completer class, which is part of dart:async.
Test above could be now written as simply as:

testWidgets('callback', (WidgetTester tester) async {
  final completer = Completer<void>();

  await tester.pumpWidget(
    MaterialApp(
      home: FlatButton(
        child: Text('press me'),
        onPressed: completer.complete,
      ),
    ),
  );

  await tester.tap(find.byType(FlatButton));

  expect(completer.isCompleted, isTrue);
});

OK, but what if you would like to also verify what argument was given to the callback… No worries, Completer also have you covered:

testWidgets('callback', (WidgetTester tester) async {
  final value = 42;
  final completer = Completer<int>();

  await tester.pumpWidget(
    MaterialApp(
// imagine that FlatButton onPressed is called with 42 as a parameter
      home: FlatButton(
        child: Text('press me'),
        onPressed: completer.complete,
      ),
    ),
  );

  await tester.tap(find.byType(FlatButton));

  expect(completer.isCompleted, isTrue);
  expect(await completer.future, equals(value));
});

What if you have more than one argument in you callback. Unfortunately Completer will not help you here, you still need to write this tiring code from the first example. But how often do you have multiple arguments in your callbacks? One workaround is to wrap all of them into a helper class… This is your design choice…


Have you knew about Completer before? Did you used it already? Are you going to use this pattern in your tests? I’m looking forward for your answers in comment section.

3 Comments Testing Flutter callbacks

  1. Hilton Pintor

    Hey, thanks so much for this!

    Also, a workaround for callbacks with multiple arguments could be something like this:

    final firstCompleter = Completer<String>();
    final secondCompleter = Completer<String>();
    
    final callback = (String firstValue, String secondValue) {
          firstCompleter.complete(firstValue);
          secondCompleter.complete(secondValue);
    };
    
    // ...
    
    expect(firstCompleter.isCompleted, isTrue);
    expect(await firstCompleter.future, expectedFirstValue);
    
    expect(secondCompleter.isCompleted, isTrue);
    expect(await secondCompleter.future, expectedSecondValue);
    
    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *