Sparround

Testing Provider-based code

Testing with Provider splits into two levels:

  • Unit tests — a ChangeNotifier is a plain Dart object, no widget needed. You call a method, count notifications with addListener, then assert on state. These tests are fast and independent of the UI.
  • Widget tests — wrap the widget in ChangeNotifierProvider.value and pass a fake model. Choosing .value matters 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 testedTest typeTool
State logic (computation, validation, error handling)Unit`test()` + `addListener`
How the UI reacts to stateWidget`testWidgets()` + `ChangeNotifierProvider.value`
Interaction with the repositoryUnit plus a fake/mock repositoryA hand-written fake or a mocking package
Leaks and disposalUnitAsserting behaviour after `dispose()`

📚 Sources and documentation