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';

/// Un'utilità di test che crea un [ProviderContainer] e lo distrugge automaticamente
/// alla fine del test
ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
// Crea un ProviderContainer, permettendo di specificare dei parametri.
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);

// Alla fine del test, distrugge il container
addTearDown(container.dispose);

return container;
}

Successivamente, possiamo definire un test utilizzando questa utilità:

void main() {
test('Some description', () {
// Crea un ProviderContainer per questo test.
// NON condividere dei ProviderContainer tra i test.
final container = createContainer();

// TODO: usare il container per testare la tua applicazione.
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(
// Equivalente di `container.read(provider)`
// Ma il provider non verrà distrutto a meno che "subscription" non venga distrutta.
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 interagire con i tuoi provider
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:

// Un provider inizializzato anticipatamente

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

Possiamo imitarlo usando:

    // Nei test unitari, riutilizzando la nostra precedente utilità "createContainer".
final container = createContainer(
// Possiamo specificare una lista di provider da emulare:
overrides: [
// In questo caso, stiamo imitando "exampleProvider".
exampleProvider.overrideWith((ref) {
// Questa funzione è la tipica funzione di inizializzazione di un provider.
// Qui è dove normalmente chiamaresti "ref.watch" e restituiresti lo stato iniziale.

// Sostituiamo il valore di default "Hello world" con un valore custom.
// Infine, quando interagiremo con `exampleProvider`, ci ritornerà questo valore.
return 'Hello from tests';
}),
],
);

// Possiamo anche fare lo stesso nei test di widget usando ProviderScope:
await tester.pumpWidget(
ProviderScope(
// I ProviderScope hanno lo stesso esatto parametro "overrides"
overrides: [
// Uguale a prima
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: usa il container per testare la tua applicazione.
// Il valore atteso è asincrono, quindi dovremmo usare "expectLater"
await expectLater(
// Leggiamo "provider.future" invece di "provider".
// Questo è possibile su provider asincroni e restituisce un future
// che si risolverà con il valore del provider.
container.read(provider.future),
// Possiamo verificare che quel future si risolva con il valore atteso.
// In alternativa possiamo usare "throwsA" per gli errori.
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();
}

// Il tuo mock necessita di subclassare la classe base del Notifier
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.