9

I have a StatefulWidget that does an async call in its initState(), to build a Widget. When I manually run this, the widget does build quickly.

However, in my test, even if I use await tester.pump() or await tester.pumpAndSettle(), the widget doesn't seem to get built, until way after the test has run.

Widget code:

Widget _coolWidget;

@override
void initState() {
  super.initState();
  _coolWidget = Container(); // If I set this to OverflowBox() my test passes
  _buildWidgetFromCanvas();
}

Future<void> _buildWidgetFromCanvas() {
  final ui.PictureRecorder recorder = ui.PictureRecorder();
  final ui.Canvas canvas = ui.Canvas(recorder);
  // ... more canvas drawing code ...
  final img = ... // Image.memory() I build from the canvas
  if (!mounted) {
    print('no longer mounted');
    return;
  }

  setState(() {
    print(img);
    _coolWidget = OverflowBox(
      child: img,
    );
    print(_coolWidget);
  });
}

Test code:

void main() {
  testWidgets('''OverflowBox shows up.''', (WidgetTester tester) async {
    await _setUp(tester); // Just instantiates my widget in an app
    await tester.pumpAndSettle();
    expect(find.byType(OverflowBox).evaluate().length, 1);
  });
}

An output when I run my test results in:

failed: Error caught by Flutter test framework, thrown running a test.
Expected: <1>
Actual: <0>

But if I set _coolWidget = OverflowBox(); in initState(), the test passes.

I have other tests that run after this one. After those ones are done, I see the print logging the print(img); and print(_coolWidget); from above, and it correctly logs the drawn image.

I also get the no longer mounted print, but that only happens as the very last print, prior to Flutter's built in (tearDownAll).

Setting durations in the pump() and pumpAndSettle() don't seem to change anything.

I'm probably missing something obvious.

1
  • Possibly related? github.com/flutter/flutter/issues/23179 Even though I'm not adding assets, I guess creating a png and then loading it from memory is kind of like an asset? Commented Nov 28, 2018 at 3:13

3 Answers 3

1

Consider using a FutureBuilder if your build method relies on async work.


Future<Image> _buildWidgetFromCanvas() {
  final ui.PictureRecorder recorder = ui.PictureRecorder();
  final ui.Canvas canvas = ui.Canvas(recorder);
  // ... more canvas drawing code ...
  if (!mounted) {
    return null
  }
  final img = ... // Image.memory() I build from the canvas
  return img;
}

FutureBuilder<Image>(
  future: _buildWidgetFromCanvas, // a previously-obtained Future<Image> or null
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    switch (snapshot.connectionState) {
      case ConnectionState.none:
      case ConnectionState.active:
      case ConnectionState.waiting:
        return SizedBox(height: <height>, width: <width>);
      case ConnectionState.done:
        return OverflowBox(child: snapshot.data);
    }
    return null; // unreachable
  },
)

You can also update your test code like this:

  expect(find.byType(OverflowBox), findsOneWidget);
Sign up to request clarification or add additional context in comments.

4 Comments

I've put the FutureBuilder<Image>(...) code into my initState(), since I want the widget to be built as early as possible. Visually it behaves as expected, though when I run a test it still doesn't seem to find my widget.
I don't think you want the FutureBuilder in your initstate. You could try to make your buildWidgetFromCanvas cache its last value and call it from initState.
Now, in initState() I have set a var, _savedFuture = _buildWidgetFromCanvas(). I've moved my FutureBuilder into build(), and my future property is now _savedFuture. It seems that the ConnectionState.done case is never reached when running the test (I have a print in that case, which is not printed).
In a separate class (another project) I was able to use FutureBuilder and test it properly!
0

I have a future builder that is stuck on waiting, and it's not finishing or throwing any errors. The futurebuilder has a future which loads a file so it should be able to do it. The app works fine but the test does not.

    // Create the widget by telling the tester to build it.
    await tester.pumpWidget(
        MaterialApp(home: DisplayPictureScreen(key: _globalKey, imagePath: 'test.png', onUpload: upload))
    );
    // Create the Finders.
    final safeAreaFinder = find.byKey(_globalKey);
    // expect we have one positioned widget
    expect(safeAreaFinder, findsOneWidget);
    await tester.pump();
    expect(find.byType(CircularProgressIndicator), findsOneWidget);
    // test code here
    await tester.pump(const Duration(seconds: 20));
    expect(find.byType(Expanded), findsOneWidget);
  });
}```

Comments

0

The pumpAndSettle method accepts a Duration argument so you can mimic the passage of time. The test doesn't actually wait for the duration to pass (eg. I can add Duration(seconds: 60) and it will finish as soon as all scheduled tasks are complete).

void main() {
  testWidgets('CounterViewModel smoke test', (WidgetTester tester) async {
    // Setup - Arrange
    await tester.pumpWidget(MaterialApp(builder: (context, child) => const DataView()));
    final DataViewModel model = tester.state(find.byType(DataViewModelBuilder));

    expect(model.name, null);

    await tester.pumpAndSettle(const Duration(seconds: 3));

    expect(model.name, 'Falco');
  });
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.