skip to content

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%

answer

  1. state lives in the container
  2. addTearDown disposes it
  3. auto-dispose may drop unread state
  4. subscription.read() keeps it alive
  5. retry: on the test container

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.

solid answer

~50 s

Riverpod keeps all provider state in a container, so a new container per test is a clean slate. `ProviderContainer.test()` — Riverpod 3's built-in replacement for the old `createContainer` helper — creates one, registers its disposal with `addTearDown`, and checks that containers were disposed; it accepts `overrides`, `observers`, `parent` and `retry`. `container.read` is fine for kept-alive providers, but an auto-dispose provider with no listener can be destroyed mid-test and lose its state. `container.listen(provider, (previous, next) {...})` keeps it alive and reports every change; the returned subscription's `read()` gives the current value, and `fireImmediately: true` also reports the initial one. For async providers, await `container.read(provider.future)`; for dependants that rebuild later, `await container.pump()`. Because Riverpod 3 retries failures, error-path tests often pass `retry: (_, _) => null`, which applies to providers that set no retry of their own.

code

dart · 34 lines
dart
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';

class ScoreNotifier extends Notifier<int> {
  @override
  int build() => 0;

  void answer({required bool correct}) {
    if (correct) state++;
  }
}

final scoreProvider =
    NotifierProvider.autoDispose<ScoreNotifier, int>(ScoreNotifier.new);

void main() {
  test('score counts only correct answers', () {
    final container = ProviderContainer.test();
    final seen = <int>[];
    final sub = container.listen<int>(
      scoreProvider,
      (previous, next) => seen.add(next),
      fireImmediately: true,
    );

    container.read(scoreProvider.notifier)
      ..answer(correct: true)
      ..answer(correct: false)
      ..answer(correct: true);

    expect(seen, [0, 1, 2]);
    expect(sub.read(), 2);
  });
}

go deeper

for a junior

Recall the pattern: create ProviderContainer.test() inside each test, read or listen to providers, and never share a container between tests.

for a middle

Explain why state lives in the container, what ProviderContainer.test registers with addTearDown, why auto-dispose providers need listen, and how to await .future.

for a senior

Show you make provider tests deterministic: record transitions with listen, pump for dependants, and control Riverpod 3's automatic retry on error paths.

for a principal

Treat container-per-test as the team's isolation guarantee, with shared helpers for overrides and retry policy so hundreds of provider tests stay fast and independent.

## Why a container per test Riverpod providers are declarations; their **state lives in a `ProviderContainer`**. Riverpod's documentation lists this as a reason for the design: tests never have to reset global state, because each test can create a new container and every provider starts fresh inside it. Sharing one container across tests reintroduces exactly the coupling the design removes — a score left at 3 by one test becomes the starting point of the next. ## `ProviderContainer.test` Riverpod 3.0 made the common Riverpod 2 helper, a hand-written `createContainer`, part of the library as **`ProviderContainer.test`**. It is a `@visibleForTesting` factory that: - creates a `ProviderContainer` with the same `overrides`, `observers`, `parent` and `retry` parameters as the plain constructor; - registers `container.dispose` with `package:test`'s `addTearDown`, so the container is disposed when the test ends, pass or fail; - adds an internal check at the end of tests that all containers were disposed. Because it relies on `addTearDown`, it works only inside a test body. Riverpod's container documentation says tests should use it rather than the plain constructor. ## `read` versus `listen` | Call | Keeps the provider alive | Reports changes | Typical use | |---|---|---|---| | `container.read(p)` | no | no | one-off reads of kept-alive providers | | `container.listen(p, listener)` | yes, until the subscription closes | yes, `(previous, next)` | auto-dispose providers, asserting transitions | | `subscription.read()` | uses the listen subscription | no | current value of a listened provider | | `container.read(p.future)` | no | no | awaiting an async provider's value | The testing guide warns about the first row: if an auto-dispose provider is not listened to, its state may be destroyed in the middle of the test, and the next `read` rebuilds it from scratch. `container.listen` adds a listener, so the provider stays mounted; the subscription's `read()` then returns the current value, equivalent to `container.read` without the risk. ## Recording transitions The listener receives `previous` and `next`. Appending `next` to a list lets a test assert the whole sequence a notifier went through. Useful parameters on `listen`: - `fireImmediately: true` also calls the listener with the initial value (with `previous` null); - `onError` receives errors thrown by the provider; - listeners are notified synchronously when a notifier assigns a new `state`, and Riverpod 3 filters notifications when the new state is `==` to the old one. For providers that depend on the changed one, rebuilds are scheduled rather than immediate; `await container.pump()` waits for them and for their listeners. ## Async providers and retry To await a `FutureProvider` or `AsyncNotifier`, read its `.future` and use `expectLater(container.read(questionsProvider.future), completion(...))`, or `throwsA(...)` for failures. Riverpod 3 **retries failing providers automatically** with exponential backoff, so a provider that throws an `Exception` in a test schedules further attempts. Error-path tests usually make that deterministic: 1. pass `retry: (_, _) => null` to `ProviderContainer.test`, which stops retries for providers without their own policy; 2. remember that a provider's own `retry:` takes precedence over the container's, so such a provider needs its own override or a fake that does not throw. ## Observers for wider assertions Both `ProviderContainer.test` and the plain constructor take an `observers` list. A `ProviderObserver` subclass sees every provider being added, updated, failing and disposed in that container, which suits assertions that span several providers — for example, that answering a question disposes the timer provider, or that no provider failed during a flow. For single-provider behaviour, `listen` stays simpler and more precise; observers are the wide-angle lens, not the default. ## A checklist for provider unit tests - One `ProviderContainer.test()` per test, never a shared one. - Overrides for every I/O dependency. - `listen` for auto-dispose providers and for sequences; `read` only for simple kept-alive values. - `.future` plus `expectLater` for async results; `container.pump()` for dependants. - An explicit retry policy on error-path tests.

  • Why might a listener in a unit test not fire right after changing a provider that another provider depends on?
    Setting a notifier's state notifies that provider's own listeners synchronously, but providers that watch it rebuild on a schedule. A listener on the dependent provider fires only after that rebuild. `await container.pump()` waits for pending rebuilds and notifications, after which the assertions on the dependent provider are reliable.
  • What does fireImmediately change in container.listen?
    Without it, the listener only hears future changes. With `fireImmediately: true`, it is also called once straight away with the current value and a null `previous`, so a list of recorded states starts with the initial one. It cannot be combined with `weak: true`, which the source asserts against.
  • Can one test use several containers?
    Yes. `ProviderContainer.test(parent: other)` builds a child container that inherits the parent's providers and adds its own overrides, which mirrors nested ProviderScopes. Each container created with `.test` is disposed at tear-down, and the end-of-test check reports any container left undisposed.

saying these in an interview costs you the question

  • One ProviderContainer shared by every test in a file is fine because providers reset themselves.
  • container.read keeps an auto-dispose provider alive for the rest of the test.
  • ProviderContainer.test automatically fakes providers that perform I/O.
  • Automatic retry never runs in tests, so error tests need no retry setting.
  • A container-level retry: always overrides a provider's own retry policy.