skip to content

Overrides & Containers

Riverpod tests swap real dependencies through overrides on a ProviderScope or a ProviderContainer, so logic runs without a widget tree. Interviewers ask how you fake a repository provider.

part ofFlutter Riverpodoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Riverpod 3, how do you swap a quiz app's repository provider for a fake in a Flutter widget test?

level: juniorimportance: must knowfreq 58%

answer

  1. wrap pumpWidget's widget in a scope
  2. the overrides list
  3. overrideWithValue with a fake instance
  4. fake the dependency, not the notifier
  5. tester.container() for reading state

basics

~20 s

Wrap the widget given to tester.pumpWidget in a ProviderScope whose overrides list replaces the repository provider, such as quizRepositoryProvider.overrideWithValue(FakeQuizRepository()). Every provider that watches the repository then builds on the fake, with no production code changes.

solid answer

~40 s

Any Riverpod provider can be overridden without extra setup. In a widget test I pump `ProviderScope(overrides: [quizRepositoryProvider.overrideWithValue(FakeQuizRepository())], child: MaterialApp(home: QuizScreen()))`. The `ProviderScope` creates the test's own `ProviderContainer`, so no state leaks between tests, and every provider that `ref.watch`es the repository — the questions `FutureProvider`, the score notifier — builds on the fake. `overrideWith((ref) => FakeQuizRepository())` works too when the fake needs `ref`. I fake the repository rather than the notifiers: Riverpod's docs discourage mocking Notifiers, which only work inside a provider and must be subclassed, never implemented. To inspect state after pumping, `tester.container()` returns the scope's container. A provider can be overridden only once per container; a duplicate trips an assertion in debug mode.

code

dart · 55 lines
dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

abstract interface class QuizRepository {
  Future<List<String>> fetchQuestions();
}

class HttpQuizRepository implements QuizRepository {
  @override
  Future<List<String>> fetchQuestions() async => throw UnimplementedError();
}

final quizRepositoryProvider =
    Provider<QuizRepository>((ref) => HttpQuizRepository());

final questionsProvider = FutureProvider<List<String>>((ref) {
  return ref.watch(quizRepositoryProvider).fetchQuestions();
});

class QuizScreen extends ConsumerWidget {
  const QuizScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return switch (ref.watch(questionsProvider)) {
      AsyncData(:final value) => Text(value.first),
      AsyncError() => const Text('Could not load quiz'),
      _ => const CircularProgressIndicator(),
    };
  }
}

class FakeQuizRepository implements QuizRepository {
  @override
  Future<List<String>> fetchQuestions() async => ['What is a widget?'];
}

void main() {
  testWidgets('shows the first question from the fake', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          quizRepositoryProvider.overrideWithValue(FakeQuizRepository()),
        ],
        child: const MaterialApp(home: Scaffold(body: QuizScreen())),
      ),
    );
    await tester.pump();

    expect(find.text('What is a widget?'), findsOneWidget);
    final container = tester.container();
    expect(container.read(questionsProvider).value, ['What is a widget?']);
  });
}

go deeper

for a junior

Remember the recipe: wrap the tested widget in ProviderScope, add the repository provider's overrideWithValue with a fake, then pump and assert on the screen.

for a middle

Explain why state lives in the container, how overrides reach every downstream provider, when overrideWith beats overrideWithValue, and what tester.container() returns.

for a senior

Show you choose the seam deliberately: override the lowest provider touching I/O, keep notifiers real, avoid duplicate overrides, and keep fakes deterministic.

for a principal

Frame provider overrides as the app's dependency-injection boundary, deciding which providers are designed as seams so tests stay fast without shaping production code around them.

## Why overrides make Riverpod testable In Riverpod, **providers hold no state themselves**. State lives in a `ProviderContainer`, and in a Flutter app the `ProviderScope` widget creates that container and exposes it to the widget tree. Because the container is the only place state lives, two things follow for tests: - every test that pumps its own `ProviderScope` starts from fresh state, with nothing carried over from the previous test; - the container accepts an **`overrides`** list, which replaces how chosen providers behave inside that container only. Riverpod's documentation stresses that all providers can be mocked this way by default, without any extra setup in production code — no service locator, no constructor injection through every widget. ## The widget-test recipe For a quiz app whose `QuizScreen` shows questions loaded through a `quizRepositoryProvider`: 1. Define a `FakeQuizRepository` that implements the same interface and returns canned questions. 2. In `testWidgets`, pump `ProviderScope(overrides: [...], child: MaterialApp(home: QuizScreen()))`. 3. Put `quizRepositoryProvider.overrideWithValue(FakeQuizRepository())` in the list. 4. Pump a frame so the fake's future completes, then assert on what the screen shows. Every provider downstream of the repository — a `questionsProvider` that calls `fetchQuestions()`, a notifier that submits answers — now reads the fake, because they obtain the repository with `ref.watch(quizRepositoryProvider)` from the same container. The production `main()` keeps its plain `ProviderScope` with no overrides. ## Which override to use | Method | Receives | Use it when | |---|---|---| | `overrideWithValue(fake)` | a ready-made value | the fake needs nothing from other providers | | `overrideWith((ref) => fake)` | a new create function with full `Ref` access | the fake must watch another provider or register `ref.onDispose` | For a repository, either works; `overrideWithValue` reads more directly. Overriding the **lowest** provider that touches the outside world — HTTP, a database, platform plugins — keeps the rest of the real logic under test. ## Override the dependency, not the notifier Riverpod's testing guide discourages mocking Notifiers. A Notifier cannot be instantiated on its own; it only works when a provider creates it. If you insist, a mock must **extend** the notifier's base class — with codegen, the generated `_$Name` class, which forces the mock into the same file — because implementing the interface breaks it. The recommended route is to put a seam below the notifier: the notifier reads `quizRepositoryProvider`, and the test overrides that. The notifier's real logic, scoring and validation, is then exercised by the test instead of being replaced by it. ## Inspecting providers from the test flutter_riverpod adds `tester.container()` to `WidgetTester`. It finds the `ProviderContainer` of the pumped scope, so a test can call `container.read(scoreProvider)` after tapping an answer, or read `container.read(questionsProvider.future)` to wait for data. Creating a separate `ProviderContainer()` in the test would not help: it is a different container with its own, unrelated state. ## Driving the screen through the fake A fake repository is more than a stub that returns data; it is how the test controls the scenario. A few fakes cover most quiz-screen tests: - a **happy** fake returning two or three questions, to check rendering and navigation between them; - an **empty** fake returning no questions, to check the empty state; - a **failing** fake that throws, to check the error message and the retry button; - a **slow** fake backed by a `Completer`, completed by the test, to check the loading indicator before and the content after. Each one is a few lines, lives in the test folder, and is passed through the same one-line override. Because the production providers stay untouched, the test also verifies the wiring between them — that `questionsProvider` really reads the repository and that the screen really reads `questionsProvider`. ## Pitfalls - **Overriding the wrong provider.** If the screen reads `questionsProvider` and the test overrides a different repository provider than the one `questionsProvider` watches, the real network code still runs. - **Duplicate overrides.** Listing the same provider twice in one container triggers an `AssertionError` in debug mode rather than letting one silently win. - **Leaking fakes between tests.** Building the fake in a shared `setUp` is fine; sharing one container across tests is not. - **Mixing real and fake state.** A fake repository that returns a new list on every call can make equality-filtered providers rebuild more than production does; return stable data.

  • Why not pass the fake repository into QuizScreen's constructor instead?
    The repository is not used by the widget directly but by providers several steps away. Threading it through constructors would change production code only for tests. With Riverpod the providers already resolve dependencies through the container, so overriding `quizRepositoryProvider` in the test's `ProviderScope` reaches every consumer without touching the widget tree's API.
  • When would you override the notifier rather than the repository?
    Rarely: when the widget test is about rendering a specific notifier state that is hard to reach through the repository. Even then, `overrideWithBuild` on the NotifierProvider can set the starting state while keeping the real methods. A full fake notifier must extend the real one, since Riverpod's docs say implementing the Notifier interface breaks it.
  • How do you override providers for an integration test of the whole app?
    Keep the app's root widget free of its own ProviderScope, or give the entry point a parameter for overrides, so the test can pump `ProviderScope(overrides: [...], child: const QuizApp())`. Nested scopes inherit from the nearest ancestor container, so the test's root scope with overrides must be the one at the top of the tree.

An override is like a stunt double on a film set: the scene, the script and the other actors stay the same, and only the one performer who would get hurt, here the network repository, is swapped for someone safe.

saying these in an interview costs you the question

  • Faking a provider requires a service locator or constructor injection in production code.
  • Listing the same provider twice in overrides lets the last one silently win.
  • A mock Notifier can simply implement the Notifier interface with a mocking library.
  • Creating a new ProviderContainer() in the test reads the same state as the pumped ProviderScope.
  • Overrides must be declared in main() and cannot differ per test.
open as a page

In Riverpod 3 unit tests, why use ProviderContainer.test and container.listen rather than a shared container and container.read?

level: middleimportance: must knowfreq 48%

basics

~20 s

ProviderContainer.test creates a fresh container per test and disposes it automatically at the end, so state never leaks. container.listen keeps auto-dispose providers alive during the test and records each change, which container.read alone does not.

open as a page

In Riverpod 3, how do overrideWith, overrideWithValue and overrideWithBuild differ when you override a provider?

level: middleimportance: should knowfreq 40%

basics

~10 s

overrideWith replaces a provider's create function and keeps full Ref access; overrideWithValue replaces its result, taking an AsyncValue for Future and Stream providers; overrideWithBuild swaps only a Notifier's build while keeping its real methods.

open as a page

In Riverpod 3, how does overriding a provider in a nested ProviderScope scope it to a subtree, and why must it declare dependencies?

level: seniorimportance: should knowfreq 28%

basics

~20 s

A nested ProviderScope creates a child container whose overrides apply only to its subtree. A provider opts into scoping by declaring dependencies, and every provider watching it must list it, or that dependant stays in the root container and never sees the override.

open as a page

In Riverpod 3.2 and later, why was a Notifier family's overrideWith deprecated for overrideWith2, and when do you override one member instead?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

overrideWith2, added in Riverpod 3.2, passes the family argument to the callback that builds a fake notifier, which the deprecated family overrideWith could not. Override one member, provider(arg).overrideWith, when only that argument should be fake.

open as a page