Flutter
widget testing
didUpdateWidget
Flutter testing
Flutter development

How to test widget that is instantiated in didUpdateWidget in Flutter?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

To test logic inside didUpdateWidget, you must trigger a real widget update on the same State object. In practice that means pumping the widget once, then pumping it again with different constructor values while keeping the widget type and key stable so Flutter reuses the existing state.

What didUpdateWidget Is For

didUpdateWidget runs when Flutter keeps the existing State instance but gives it a new widget configuration from the parent. It does not run because the widget called setState on itself, and it does not run when the old state is destroyed and replaced.

That lifecycle detail shapes the test strategy:

  • same widget class,
  • same key,
  • different incoming properties,
  • and a second pump from the test.

If you change the key or change the widget type, Flutter creates a new State object and the old one never receives didUpdateWidget.

Example Widget That Recreates a Controller

Here is a small widget that rebuilds a TextEditingController whenever the label prop changes:

dart
1import 'package:flutter/material.dart';
2
3class DemoField extends StatefulWidget {
4  const DemoField({
5    super.key,
6    required this.label,
7  });
8
9  final String label;
10
11  
12  State<DemoField> createState() => _DemoFieldState();
13}
14
15class _DemoFieldState extends State<DemoField> {
16  late TextEditingController controller;
17
18  
19  void initState() {
20    super.initState();
21    controller = TextEditingController(text: widget.label);
22  }
23
24  
25  void didUpdateWidget(covariant DemoField oldWidget) {
26    super.didUpdateWidget(oldWidget);
27    if (oldWidget.label != widget.label) {
28      controller.dispose();
29      controller = TextEditingController(text: widget.label);
30    }
31  }
32
33  
34  void dispose() {
35    controller.dispose();
36    super.dispose();
37  }
38
39  
40  Widget build(BuildContext context) {
41    return MaterialApp(
42      home: Scaffold(
43        body: TextField(controller: controller),
44      ),
45    );
46  }
47}

The goal of the test is not merely to prove the widget can rebuild. It is to prove the update path went through didUpdateWidget.

Test by Pumping Twice

The simplest and most reliable pattern is a two-pump test:

dart
1import 'package:flutter/material.dart';
2import 'package:flutter_test/flutter_test.dart';
3
4void main() {
5  testWidgets('updates controller when label changes', (tester) async {
6    const key = ValueKey('demo-field');
7
8    await tester.pumpWidget(
9      const DemoField(key: key, label: 'before'),
10    );
11    expect(find.text('before'), findsOneWidget);
12
13    await tester.pumpWidget(
14      const DemoField(key: key, label: 'after'),
15    );
16    await tester.pump();
17
18    expect(find.text('after'), findsOneWidget);
19    expect(find.text('before'), findsNothing);
20  });
21}

The stable key is important because it helps Flutter match the new widget instance to the existing stateful element.

Verify Side Effects, Not Just Pixels

Sometimes the thing instantiated in didUpdateWidget is not directly visible. You may be replacing:

  • a stream subscription,
  • an animation controller,
  • a focus node,
  • or a callback binding to an external object.

In that case, assert the observable side effect rather than trying to inspect lifecycle methods directly.

One good pattern is to inject a factory or dependency that your test can observe:

dart
typedef LabelControllerFactory = TextEditingController Function(String text);

If the widget calls that factory from didUpdateWidget, the test can count calls or inspect the created object. That is often cleaner than exposing private state just to make the test pass.

Keep the Parent-Update Mental Model

It helps to think of didUpdateWidget as a parent-driven event. The parent rebuilds, the framework decides the existing child state can be kept, and then the child receives new widget data.

That means a widget test for didUpdateWidget should mimic a parent rebuild. The second pumpWidget call is your parent rebuilding the subtree with new properties.

If you instead call methods on the state directly, you are no longer testing the lifecycle behavior that matters.

When You Need More Than One pump

If didUpdateWidget starts an animation or async callback, the widget tree may need extra time to settle. In those cases use:

  • 'pump()'
  • 'pump(const Duration(...))'
  • or pumpAndSettle()

The exact choice depends on whether the side effect is immediate, frame-based, or asynchronous.

Design for Easier Testing

If testing didUpdateWidget feels awkward, that can be a signal that the widget owns too much replaceable state. Not everything belongs in a lifecycle method. If a child can be rebuilt cheaply from current props inside build, that is often simpler than caching a resource and carefully updating it later.

Lifecycle hooks are appropriate when you truly need to synchronize stateful resources with changing widget configuration.

Common Pitfalls

The most common mistake is changing the key between pumps. That causes Flutter to throw away the old State, so the test never exercises didUpdateWidget.

Another mistake is expecting didUpdateWidget to run after setState inside the same widget. It runs when a parent provides a new widget configuration, not when the state mutates itself.

Developers also often assert only final visible text when the real behavior is a side effect such as resubscribing to a stream. Test the effect that actually proves the lifecycle hook ran.

Finally, if the hook triggers asynchronous work, do not assert too early. Add the extra pump calls needed for the update to complete.

Summary

  • Trigger didUpdateWidget by pumping the same widget type again with new inputs.
  • Keep the same key so Flutter reuses the existing State.
  • Assert side effects that prove the update path ran, not just generic rebuild behavior.
  • Remember that didUpdateWidget is parent-driven, not caused by the widget's own setState.
  • If the logic is hard to test, reconsider whether the widget is holding unnecessary mutable state.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.