Salta al contenuto principale

Testare i tuoi provider

Una parte fondamentale delle API di Riverpod è l'abilità di testare i tuoi provider in modo isolato.

Per una suite di test adeguata, ci sono alcune sfide da superare:

  • I test non dovrebbero condividere lo stato. Ciò significa che nuovi test non dovrebbero essere influenzati dai test precedenti.
  • I test dovrebbero darci l'abilità di emulare certe funzionalità per ottenere lo stato desiderato.
  • L'ambiente di test dovrebbe essere il più vicino possibile all'ambiente reale.

Fortunatamente, Riverpod semplifica il raggiungimento di tutti questi obiettivi.

Impostare un test

Quando si definisce un test con Riverpod, ci sono due scenari principali:

  • Test unitari, di solito senza dipendenze di Flutter. Possono essere utili per testare il comportamento di un provider isolamente.
  • Test di widget, di solito con dipendenze di Flutter. Possono essere utili per testare il comportamento di un widget che utilizza un provider.

Test unitari

I test unitari sono definit usando la funzione test da package:test

La differenza principale con qualsiasi altro test è che creeremo un oggetto ProviderContainer. Questo oggetto permetterà al nostro test di interagire con i provider

Si consiglia di creare un'utilità di test sia per la creazione che per l'eliminazione di un oggetto ProviderContainer:

import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';

/// A testing utility which creates a [ProviderContainer] and automatically
/// disposes it at the end of the test.
ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
// Create a ProviderContainer, and optionally allow specifying parameters.
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);

// When the test ends, dispose the container.
addTearDown(container.dispose);

return container;
}

Successivamente, possiamo definire un test utilizzando questa utilità:

void main() {
test('Some description', () {
// Create a ProviderContainer for this test.
// DO NOT share ProviderContainers between tests.
final container = createContainer();

// TODO: use the container to test your application.
expect(
container.read(provider),
equals('some value'),
);
});
}

Ora che abbiamo un ProviderContainer possiamo utilizzarlo per leggere i provider usando:

  • container.read, per leggere il valore corrente di un provider.
  • container.listen, per restare in ascolto di un provider ed essere notificato dei suoi cambiamenti.
caution

Fai attenzione quando usi container.read quando i provider sono distrutti automaticamente. Se il tuo provider non è ascoltato, ci sono chances che il suo stato verrà distrutto nel mezzo del nostro test.

In quel caso, considera utilizzare container.listen. Il suo valore di ritorno permette comunque di leggere il valore corrente del provider, ma si assicurerà anche che il provider non venga distrutto nel mezzo del tuo test:

    final subscription = container.listen<String>(provider, (_, __) {});

expect(
// Equivalent to `container.read(provider)`
// But the provider will not be disposed unless "subscription" is disposed.
subscription.read(),
'Some value',
);

Test di widget

I test dei widget sono definiti usando la funzione testWidgets da package:flutter_test.

In questo caso, la differenza principale con i normali test di widget è che dobbiamo aggiungere un widget ProviderScope alla radice di tester.pumpWidget.

void main() {
testWidgets('Some description', (tester) async {
await tester.pumpWidget(
const ProviderScope(child: YourWidgetYouWantToTest()),
);
});
}

Questo è simile a quello che facciamo quando abilitiamo Riverpod nella nostra app Flutter.

Successivamente, possiamo usare tester per interagire col nostro widget. In alternativa, se vuoi interagire coi tuoi provider, puoi ottenere un ProviderContainer. Un oggetto ProviderContainer può essere ottenuto usando ProviderScope.containerOf(buildContext). Usando tester possiamo quindi scrivere quanto segue:

    final element = tester.element(find.byType(YourWidgetYouWantToTest));
final container = ProviderScope.containerOf(element);

Possiamo quindi usarlo per leggere i provider. Di seguito un esempio completo:

void main() {
testWidgets('Some description', (tester) async {
await tester.pumpWidget(
const ProviderScope(child: YourWidgetYouWantToTest()),
);

final element = tester.element(find.byType(YourWidgetYouWantToTest));
final container = ProviderScope.containerOf(element);

// TODO interact with your providers
expect(
container.read(provider),
'some value',
);
});
}

Mock/Imitare provider

Fino ad ora abbiamo visto come impostare un test ed interagire in modo semplice con i provider. Tuttavia, in alcuni casi, potremmo voler imitare un provider.

La parte interessante: tutti i provider possono essere imitati di default, senza nessun impostazione aggiuntiva. Questo è possibile specificando il parametro overrides su ProviderScope o ProviderContainer.

Consideriamo il provider seguente:

// An eagerly initialized provider.

Future<String> example(ExampleRef ref) async => 'Hello world';

Possiamo imitarlo usando:

    // In unit tests, by reusing our previous "createContainer" utility.
final container = createContainer(
// We can specify a list of providers to mock:
overrides: [
// In this case, we are mocking "exampleProvider".
exampleProvider.overrideWith((ref) {
// This function is the typical initialization function of a provider.
// This is where you normally call "ref.watch" and return the initial state.

// Let's replace the default "Hello world" with a custom value.
// Then, interacting with `exampleProvider` will return this value.
return 'Hello from tests';
}),
],
);

// We can also do the same thing in widget tests using ProviderScope:
await tester.pumpWidget(
ProviderScope(
// ProviderScopes have the exact same "overrides" parameter
overrides: [
// Same as before
exampleProvider.overrideWith((ref) => 'Hello from tests'),
],
child: const YourWidgetYouWantToTest(),
),
);

Spiare i cambiamenti in un provider

Dato che abbiamo ottenuto un ProviderContainer nei nostri test, è possibile usarlo per "ascoltare" un provider:

    container.listen<String>(
provider,
(previous, next) {
print('The provider changed from $previous to $next');
},
);

Puoi combinare questo con pacchetti come mockito o mocktail per usare la loro API verify. O più semplicemente, puoi aggiungere tutti i cambiamenti in una lista e controllarli tramite 'assert'.

Aspettare provider asincroni

In Riverpod è molto comune per i provider restituire un Future/Stream. In questo caso, ci sono chances che i nostri test abbiano bisogno di aspettare che quelle operazioni asincrone siano completate.

Un modo per farlo è leggere il .future di un provider:

    // TODO: use the container to test your application.
// Our expectation is asynchronous, so we should use "expectLater"
await expectLater(
// We read "provider.future" instead of "provider".
// This is possible on asynchronous providers, and returns a future
// which will resolve with the value of the provider.
container.read(provider.future),
// We can verify that the future resolves with the expected value.
// Alternatively we can use "throwsA" for errors.
completion('some value'),
);

Imitare i Notifier

È generalmente sconsigliato imitare i Notifier. Invece, dovresti probabilmente introdurre un livello di astrazione nella logica del tuo Notifier, in modo tale da poter imitare tale astrazione. Per esempio, al posto di imitare un Notifier, potresti imitare un "repository" che il Notifier usa per ottenere i dati.

Se vuoi insistere nell'imitare un Notifier, esiste una considerazione speciale per creare un mock di questo tipo: il tuo mock deve essere una subclass della classe base del Notifier: non puoi implementare (via "implements") un Notifier, poichè romperebbe l'interfaccia.

Pertanto, quando si imita un Notifier, invece di scrivere il codice mockito seguente:

class MyNotifierMock with Mock implements MyNotifier {}

Dovresti invece scrivere:


class MyNotifier extends _$MyNotifier {

int build() => throw UnimplementedError();
}

// Your mock needs to subclass the Notifier base-class corresponding
// to whatever your notifier uses
class MyNotifierMock extends _$MyNotifier with Mock implements MyNotifier {}

Per far sì che funzioni, il tuo mock deve essere posto nello stesso file del Notifier che stai imitando. Altrimenti, non avresti accesso alla classe _$MyNotifier.