Testing Provider-based code
Testing with Provider splits into two levels:
- Unit tests — a
ChangeNotifieris a plain Dart object, no widget needed. You call a method, count notifications withaddListener, then assert on state. These tests are fast and independent of the UI. - Widget tests — wrap the widget in
ChangeNotifierProvider.valueand pass a fake model. Choosing.valuematters here: the test creates the model and owns its lifetime.
A third, often forgotten level is the notifier's disposal behaviour: a test asserting that subscriptions stop after disposal prevents real leaks.
dart
import 'package:flutter_test/flutter_test.dart';
void main() {
test('add() elementi əlavə edir və bir dəfə bildiriş verir', () {
final model = CartModel();
var notifications = 0;
model.addListener(() => notifications++);
model.add(const Item(id: '1', price: 10));
expect(model.items, hasLength(1));
expect(notifications, 1); // lazımsız bildirişləri tutur
});
test('eyni elementi ikinci dəfə əlavə etmək bildiriş vermir', () {
final model = CartModel()..add(const Item(id: '1', price: 10));
var notifications = 0;
model.addListener(() => notifications++);
model.add(const Item(id: '1', price: 10));
expect(notifications, 0);
});
}A unit test for the notifier, asserting the number of notifications too.
dart
class FakeCartModel extends ChangeNotifier implements CartModel {
@override
List<Item> items = const [];
@override
double get totalPrice => items.fold(0, (sum, i) => sum + i.price);
void setItems(List<Item> value) {
items = value;
notifyListeners();
}
@override
void add(Item item) => setItems([...items, item]);
}
void main() {
testWidgets('səbətin cəmi göstərilir və yenilənir', (tester) async {
final fake = FakeCartModel();
await tester.pumpWidget(
ChangeNotifierProvider<CartModel>.value(
value: fake,
child: const MaterialApp(home: CartSummary()),
),
);
expect(find.text('Cəmi: 0.0'), findsOneWidget);
fake.setItems([const Item(id: '1', price: 12.5)]);
await tester.pump(); // bildirişdən sonra bir frame
expect(find.text('Cəmi: 12.5'), findsOneWidget);
});
}A widget test: the fake model is passed with `.value`.
If a widget test uses ChangeNotifierProvider(create: ...), the provider disposes the model when the test tears down — which can produce "used after disposed" if a later step in the same test touches that object. Let the test own the model: .value, plus addTearDown(fake.dispose) where needed.
| What is tested | Test type | Tool |
|---|---|---|
| State logic (computation, validation, error handling) | Unit | `test()` + `addListener` |
| How the UI reacts to state | Widget | `testWidgets()` + `ChangeNotifierProvider.value` |
| Interaction with the repository | Unit plus a fake/mock repository | A hand-written fake or a mocking package |
| Leaks and disposal | Unit | Asserting behaviour after `dispose()` |
📚 Sources and documentation
- Flutter: an introduction to widget testingofficialdocs.flutter.dev
The official explanation of testWidgets, pumpWidget, pump and finders.
- Flutter: unit testsofficialdocs.flutter.dev
- package:providerofficialpub.dev
The section confirming the .value constructor is for existing objects.