Kodokon kodokon.com

Tests: Dart-Unit-Tests und Widget-Tests

Teste Logik und Widgets mit handgeschriebenen Fakes, einer simulierten Uhr und den richtigen pump-Reflexen.

11 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

Die Testpyramide von Flutter: eine breite Basis aus reinen Dart-Unit-Tests, eine Schicht Widget-Tests, ein paar Integrationstests. Ein Unit-Test sollte niemals material.dart importieren - wenn deine Logik das verlangt, ist das Problem architektonisch, nicht technisch. Um Code von seinen Abhängigkeiten zu isolieren, sind handgeschriebene Fakes generierten Mocks vorzuziehen: Ein Fake ist eine vereinfachte, aber wirklich funktionierende Umsetzung des Vertrags (Repository im Speicher, steuerbare Uhr); es übersteht Refactorings und liest sich wie ganz normaler Code. Mocks, die Interaktionen prüfen ("diese Methode wurde zweimal aufgerufen"), koppeln den Test an Implementierungsdetails - hebe sie dir für Protokolle auf, bei denen die Interaktion das Verhalten ist.

DART
import 'package:flutter/foundation.dart';

abstract interface class ScoreRepository {
  Future<int> fetchBest();
}

class ScoreController extends ChangeNotifier {
  ScoreController(this._repository);

  final ScoreRepository _repository;

  int? best;
  Object? error;

  Future<void> load() async {
    try {
      best = await _repository.fetchBest();
      error = null;
    } on Exception catch (e) {
      error = e;
    }
    notifyListeners();
  }
}
Der zu testende Code: kein Import von material.dart.

setUp erstellt die Objekte vor jedem Test neu: kein geteilter Zustand, also kein Test, der von der Reihenfolge der Ausführung abhängt - genau das macht Tests verlässlich. Kombinierbare Matcher (isA<Exception>(), throwsA, isNull) liefern präzise Fehlermeldungen, die weit nützlicher sind als ein Wahrheitswert. Achte auf den zweiten Test unten: Er prüft den Fehlschlag und direkt danach die Erholung. Fehlerpfade sind Code wie jeder andere - wenn sie nicht getestet sind, betrachte sie als kaputt.

DART
import 'package:flutter_test/flutter_test.dart';

class FakeScoreRepository implements ScoreRepository {
  int calls = 0;
  bool failNextCall = false;

  @override
  Future<int> fetchBest() async {
    calls++;
    if (failNextCall) {
      failNextCall = false;
      throw Exception('network unavailable');
    }
    return 42;
  }
}

void main() {
  late FakeScoreRepository repository;
  late ScoreController controller;

  setUp(() {
    repository = FakeScoreRepository();
    controller = ScoreController(repository);
  });

  test('exposes the best score', () async {
    await controller.load();

    expect(controller.best, 42);
    expect(controller.error, isNull);
    expect(repository.calls, 1);
  });

  test('captures an error then recovers', () async {
    repository.failNextCall = true;

    await controller.load();
    expect(controller.error, isA<Exception>());

    await controller.load();
    expect(controller.best, 42);
    expect(controller.error, isNull);
  });

  test('notifies its listeners', () async {
    var notifications = 0;
    controller.addListener(() => notifications++);

    await controller.load();

    expect(notifications, 1);
  });
}
Ein Fake aus fünfzehn Zeilen ersetzt eine ganze Bibliothek voller Mocks.

Ein Widget-Test zeichnet nichts auf den Bildschirm: Er läuft in einem spezialisierten Binding (AutomatedTestWidgetsFlutterBinding), in dem die Zeit simuliert wird und in dem jeder Frame auf Anforderung erzeugt wird. pumpWidget hängt den Baum ein; pump() stellt die simulierte Uhr weiter und erzeugt einen Frame; pump(const Duration(milliseconds: 150)) springt in der Zeit - ideal, um einen Zwischenzustand einer Animation einzufrieren und zu untersuchen. Die Finder (find.text, find.byType, find.byKey) durchsuchen den echten Baum der Elements, keinen Screenshot. Weil nichts wirklich asynchron ist - weder Uhr noch Netzwerk -, sind diese Tests deterministisch: Ein Fehlschlag ist immer reproduzierbar.

DART
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class BadgeScreen extends StatefulWidget {
  const BadgeScreen({super.key});

  @override
  State<BadgeScreen> createState() => _BadgeScreenState();
}

class _BadgeScreenState extends State<BadgeScreen> {
  bool _visible = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          if (_visible) const Text('New'),
          IconButton(
            icon: const Icon(Icons.close),
            onPressed: () =>
                setState(() => _visible = false),
          ),
        ],
      ),
    );
  }
}

void main() {
  testWidgets('hides the badge after the tap',
      (tester) async {
    await tester.pumpWidget(
      const MaterialApp(home: BadgeScreen()),
    );
    expect(find.text('New'), findsOneWidget);

    await tester.tap(find.byIcon(Icons.close));
    await tester.pump();

    expect(find.text('New'), findsNothing);
  });
}
tap löst keinen Frame aus: Das darauf folgende pump ist Pflicht.
BASH
flutter test
flutter test test/score_controller_test.dart
flutter test --coverage
flutter test --update-goldens
Eine Datei gezielt testen, die Abdeckung messen, die Goldens neu erzeugen.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Warum schlägt pumpAndSettle bei einer mit repeat() gestarteten Animation fehl?
    • Es pumpt Frames, solange noch welche eingeplant sind: Eine unendliche Animation stabilisiert sich nie, daher ein Timeout
    • repeat() wird vom Test-Binding nicht unterstützt
    • pumpAndSettle kommt nur mit impliziten Animationen zurecht
  2. Was ist der Unterschied zwischen einem Fake und einem Mock?
    • Ein Fake ist eine vereinfachte, aber funktionierende Umsetzung; ein Mock dient vor allem dazu, Interaktionen zu prüfen
    • Der Mock wird von Hand geschrieben, das Fake von einem Werkzeug erzeugt
    • Keiner: Die beiden Begriffe sind gleichbedeutend
    • Ein Fake kann keine Dart-Schnittstelle implementieren
  3. Was macht await tester.pump() in einem Widget-Test genau?
    • Es stellt die simulierte Uhr weiter und erzeugt einen einzigen neuen Frame
    • Es wartet, bis alle laufenden Animationen beendet sind
    • Es startet den Test von vorn
    • Es wartet auf das Ende echter Netzwerkanfragen