Dart Unit-Tests

Unit-Testing ist ein grundlegendes Mittel zur Sicherung der Codequalität.

Es überprüft, ob das Verhalten jeder kleinsten funktionalen Einheit (normalerweise eine Funktion oder Methode) den Erwartungen entspricht.

Dieses Kapitel führt in die Verwendung des test-Pakets von Dart ein, das Schreiben von Testfällen mit test(), Assertions mit expect() und das Gruppieren von Tests mit group().


Installation und Konfiguration des test-Pakets

Dart stellt offiziell das test-Paket zum Schreiben und Ausführen von Tests bereit.

Fügen Sie zuerst die Abhängigkeit in pubspec.yaml hinzu:

# 文件路径:pubspec.yaml
dev_dependencies:
  test: ^1.24.0

Führen Sie dann den folgenden Befehl zur Installation aus:

$ dart pub get

Testdateien werden üblicherweise im test/-Verzeichnis des Projekts abgelegt, wobei die Dateinamen auf _test.dart enden.

Eine typische Projektstruktur:

my_project/
├── lib/
│   └── calculator.dart       # 被测试的代码
├── test/
│   └── calculator_test.dart  # 测试文件
└── pubspec.yaml

Testfälle mit test() schreiben

Die Funktion test() ist die grundlegende Einheit zum Schreiben von Testfällen.

Sie nimmt zwei Parameter entgegen: eine Testbeschreibung (String) und einen Testfunktionskörper.

Beispiel

Der zu testende Code (lib/calculator.dart):

// Dateipfad: lib/calculator.dart
class Calculator {
  int add(int a, int b) => a + b;
  int subtract(int a, int b) => a - b;
  int multiply(int a, int b) => a * b;

  double divide(int a, int b) {
    if (b == 0) {
      throw ArgumentError(Der Divisor darf nicht 0 sein);
    }
    return a / b;
  }

  bool isEven(int n) => n % 2 == 0;

  List<int> filterPositive(List<int> numbers) {
    return numbers.where((n) => n > 0).toList();
  }
}

Beispiel

Testdatei (test/calculator_test.dart):

// Dateipfad: test/calculator_test.dart
import 'package:test/test.dart';
import 'package:my_project/calculator.dart';

void main() {
  // Eine Calculator-Instanz zum Testen erstellen
  var calculator = Calculator();

  // test()-Funktion: Der erste Parameter ist die Testbeschreibung, der zweite ist die Testfunktion
  test('add() addiert zwei positive Zahlen', () {
    var result = calculator.add(3, 5);
    // expect()-Assertion: prüft, ob das tatsächliche Ergebnis dem erwarteten Wert entspricht
    expect(result, equals(8));
  });

  test('add() addiert negative Zahlen', () {
    expect(calculator.add(-3, 5), equals(2));
    expect(calculator.add(-3, -5), equals(-8));
  });

  test('subtract() Subtraktionsoperation', () {
    expect(calculator.subtract(10, 3), equals(7));
  });

  test('multiply() Multiplikationsoperation', () {
    expect(calculator.multiply(4, 5), equals(20));
    // Mit null multiplizieren
    expect(calculator.multiply(100, 0), equals(0));
  });

  test(divide() normale Division, () {
    expect(calculator.divide(10, 2), equals(5.0));
    expect(calculator.divide(7, 2), equals(3.5));
  });

  test(isEven() Prüfung auf gerade Zahl, () {
    expect(calculator.isEven(4), isTrue);
    expect(calculator.isEven(5), isFalse);
  });
}

Tests ausführen:

$ dart test
00:00 +6: All tests passed!

expect-Assertions

expect() ist die grundlegendste Funktion beim Testen; sie prüft, ob ein Ist-Wert eine Bedingung erfüllt.

Grundlegende Syntax: expect(actual, matcher).

Häufige Matcher

MatcherZweckBeispiel
equals(expected)Prüft, ob Werte gleich sindexpect(result, equals(42))
isTrue / isFalseÜberprüft boolesche Werteexpect(flag, isTrue)
isNull / isNotNullNull überprüfenexpect(value, isNull)
contains)Enthält ein Element (Liste) oder eine Teilzeichenfolge (String).expect(list, c('a'))
isA()Typen überprüfenexpect(obj, isA())
throws()Überprüfen, ob eine Ausnahme ausgelöst wurdeexpect(() => f(), throwsException)
isNotEmptynicht nullexpect(list, isNotEmpty)
hasLength(n)Länge überprüfenexpect(list, hasLength(3))
greaterThan(n)größer alsexpect(score, greaterThan(60))
closeTo(value, delta)Gleitkommazahlen ungefähr gleichexpect(3.14, closeTo(3.1, 0.1))

Beispiel

Praktische Anwendungen verschiedener Matcher:

import 'package:test/test.dart';

void main() {
  test(BEISPIEL Verschiedene Assertion-Beispiele, () {
    // Grundlegende Gleichheit
    expect(2 + 2, equals(4));

    // Boolesche Werte
    expect('hello'.contains('h'), isTrue);
    expect(''.isEmpty, isTrue);

    // Nullprüfung
    String? name;
    expect(name, isNull);
    name = 'EXAMPLE';
    expect(name, isNotNull);

    // Typprüfung
    expect('EXAMPLE', isA<String>());
    expect(42, isA<int>());

    // Liste/Zeichenkette enthält
    expect([1, 2, 3], contains(2));
    expect('Hello, EXAMPLE!', contains('EXAMPLE'));

    // Länge
    expect([1, 2, 3], hasLength(3));
    expect('Dart', hasLength(4));

    // Numerischer Vergleich
    expect(100, greaterThan(50));
    expect(30, lessThan(60));
    expect(75, greaterThanOrEqualTo(60));

    // Gleitkommavergleich (Präzisionsprobleme vermeiden)
    expect(0.1 + 0.2, closeTo(0.3, 0.001));

    // Sammlung ist nicht leer
    expect([1, 2], isNotEmpty);

    // List-Gleichheit
    expect([1, 2, 3], equals([1, 2, 3]));

    // Map enthält einen Schlüssel
    var user = {'name': 'example', 'age': 10};
    expect(user, containsPair('name', 'example'));
    expect(user.keys, contains('age'));
  });
}
00:00 +1: All tests passed!

Ausnahmen testen

Beispiel

import 'package:test/test.dart';

int divide(int a, int b) {
  if (b == 0) throw ArgumentError(Divisor darf nicht 0 sein);
  return a ~/ b;
}

void main() {
  test(divide() sollte beim Teilen durch null eine Ausnahme auslösen, () {
    // Überprüfen, dass eine beliebige Ausnahme ausgelöst wird
    expect(() => divide(10, 0), throwsException);

    // Überprüfen, dass ein bestimmter Ausnahmetyp ausgelöst wird
    expect(() => divide(10, 0), throwsArgumentError);

    // Ausnahmemeldung überprüfen
    expect(
      () => divide(10, 0),
      throwsA(predicate((e) =>
          e is ArgumentError &&
          e.message.contains(divisor))),
    );
  });

  test(divide() löst in normalen Fällen keine Ausnahme aus, () {
    expect(() => divide(10, 2), returnsNormally);
    expect(divide(10, 2), equals(5));
  });
}
00:00 +2: All tests passed!

Beim Testen von Ausnahmen müssen Sie den Code, der eine Ausnahme auslösen kann, in eine anonyme Funktion (() => code) einschließen, anstatt ihn direkt aufzurufen. Wenn Sie expect(divide(10, 0), throwsException) direkt schreiben, löst divide sofort eine Ausnahme aus, und expect erhält nie die Gelegenheit, ausgeführt zu werden.


Tests mit group() gruppieren

group() wird verwendet, um zusammengehörige Testfälle zu organisieren und die Teststruktur klarer zu machen.

Beispiel

import 'package:test/test.dart';

// Zu testende Funktion
class StringUtils {
  static String capitalize(String s) {
    if (s.isEmpty) return s;
    return s[0].toUpperCase() + s.substring(1);
  }

  static String reverse(String s) {
    return s.split('').reversed.join('');
  }

  static bool isPalindrome(String s) {
    var clean = s.toLowerCase().replaceAll(' ', '');
    return clean == reverse(clean);
  }

  static int countWords(String s) {
    if (s.trim().isEmpty) return 0;
    return s.trim().split(RegExp(r'\s+')).length;
  }
}

void main() {
  // group() verschachtelte Testorganisation
  group('StringUtils', () {
    // Untergruppe
    group('capitalize()', () {
      test(Schreibt den ersten Buchstaben eines normalen Wortes groß, () {
        expect(StringUtils.capitalize('hello'), equals('Hello'));
      });

      test(Ein bereits großgeschriebenes Wort bleibt unverändert, () {
        expect(StringUtils.capitalize('Hello'), equals('Hello'));
      });

      test(Ein leerer String gibt einen leeren String zurück, () {
        expect(StringUtils.capitalize(''), equals(''));
      });

      test(Einzelnes Zeichen, () {
        expect(StringUtils.capitalize('a'), equals('A'));
      });
    });

    group('reverse()', () {
      test(Kehrt eine normale Zeichenkette um, () {
        expect(StringUtils.reverse('EXAMPLE'), equals('BOONUR'));
      });

      test(Eine Palindrom-Zeichenkette bleibt beim Umkehren unverändert, () {
        expect(StringUtils.reverse('aba'), equals('aba'));
      });

      test(Leere Zeichenkette, () {
        expect(StringUtils.reverse(''), equals(''));
      });
    });

    group('isPalindrome()', () {
      test(Palindrom-Zeichenkette gibt true zurück, () {
        expect(StringUtils.isPalindrome('racecar'), isTrue);
        expect(StringUtils.isPalindrome('A man a plan a canal Panama'), isTrue);
      });

      test(Nicht-Palindrom-Zeichenkette gibt false zurück, () {
        expect(StringUtils.isPalindrome('hello'), isFalse);
      });

      test(Eine leere Zeichenkette gilt als Palindrom, () {
        expect(StringUtils.isPalindrome(''), isTrue);
      });
    });

    group('countWords()', () {
      test(Normaler Satz, () {
        expect(StringUtils.countWords('Hello World Dart'), equals(3));
      });

      test(Zusätzliche Leerzeichen, () {
        expect(StringUtils.countWords(' Hello World '), equals(2));
      });

      test(Leere Zeichenkette, () {
        expect(StringUtils.countWords(''), equals(0));
        expect(StringUtils.countWords('   '), equals(0));
      });
    });
  });
}
00:00 +12: All tests passed!

group() kann verschachtelt werden, um eine hierarchische Teststruktur zu erstellen.

Dies macht Testberichte leichter lesbar und hilft außerdem, fehlschlagende Tests schnell zu finden.

Eine gute Gruppierungsstrategie ist es, nach dem "Modul/Klasse/Methode im Test" zu organisieren. Auf diese Weise können Sie bei einem fehlgeschlagenen Test sofort erkennen, welche Funktionalität ein Problem hat.

setUp und tearDown

setUp läuft vor jedem Test, tearDown läuft nach jedem Test.

Sie werden verwendet, um die Testumgebung vorzubereiten und Ressourcen aufzuräumen.

Beispiel

import 'package:test/test.dart';

// Simuliere eine Klasse, die Initialisierung und Bereinigung benötigt
class Database {
  bool isConnected = false;

  void connect() {
    isConnected = true;
    print(Database connected);
  }

  void disconnect() {
    isConnected = false;
    print(Datenbank getrennt);
  }

  String query(String sql) {
    if (!isConnected) throw Exception(Datenbank nicht verbunden);
    return Abfrageergebnis: $sql;
  }
}

void main() {
  group(Datenbank-Test, () {
    late Database db;  // späte Lazy-Initialisierung

    // Vor jedem Test ausführen
    setUp(() {
      db = Database();
      db.connect();
      print([setUp] Testumgebung vorbereiten);
    });

    // Nach jedem Test ausführen
    tearDown(() {
      db.disconnect();
      print([tearDown] Testumgebung aufräumen);
    });

    test(query() normale Abfrage, () {
      var result = db.query('SELECT * FROM users');
      expect(result, contains('EXAMPLE') ? result.contains('users') : result.contains('users'));
      // Assertions vereinfachen
      expect(result, isNotEmpty);
    });

    test(query() kann mehrere Abfragen nach dem Verbinden ausführen, () {
      var r1 = db.query('SELECT 1');
      var r2 = db.query('SELECT 2');
      expect(r1, isNotEmpty);
      expect(r2, isNotEmpty);
    });

    test(Abfrage löst eine Ausnahme aus, wenn keine Verbindung besteht, () {
      db.disconnect();  // Manuell trennen
      expect(() => db.query('SELECT 1'), throwsException);
    });
  });
}
  数据库已连接
  [setUp] 准备测试环境
  数据库已断开
  [tearDown] 清理测试环境
  数据库已连接
  [setUp] 准备测试环境
  数据库已断开
  [tearDown] 清理测试环境
  数据库已连接
  [setUp] 准备测试环境
  数据库已断开
  [tearDown] 清理测试环境
00:00 +3: All tests passed!

Beachte die Ausführungsreihenfolge von setUp und tearDown in der Ausgabe – sie werden einmal vor und nach jedem Test ausgeführt.

setUp und tearDown stellen sicher, dass jeder Test von einem sauberen Zustand aus startet und nicht von anderen Tests beeinflusst wird. Dies ist eine zentrale Garantie für die Testunabhängigkeit.


Asynchroner Test

Das Dart-Testpaket unterstützt asynchrones Testen nativ.

Eine Testfunktion kann ein Future zurückgeben, und das Framework wartet automatisch, bis das Future abgeschlossen ist.

Beispiel

import 'package:test/test.dart';

// Asynchrone Funktion: Netzwerkanfrage simulieren
Future<String> fetchUserData(int userId) async {
  await Future.delayed(Duration(milliseconds: 100));

  if (userId <= 0) {
    throw Exception(Ungültige Benutzer-ID);
  }

  return Daten von Benutzer $userId;
}

Future<List<int>> fetchNumbers() async {
  await Future.delayed(Duration(milliseconds: 50));
  return [1, 2, 3, 4, 5];
}

void main() {
  group(Asynchroner Test, () {
    test(fetchUserData() ruft Daten normal ab, () async {
      var data = await fetchUserData(1);
      expect(data, equals(Daten von Benutzer 1));
    });

    test(fetchUserData() wirft eine Ausnahme für ungültige ID, () async {
      expect(
        () => fetchUserData(-1),
        throwsA(isA<Exception>()),
      );
    });

    test('fetchNumbers() gibt die korrekte Liste zurück', () async {
      var numbers = await fetchNumbers();
      expect(numbers, hasLength(5));
      expect(numbers, contains(3));
      expect(numbers.first, equals(1));
    });

    // Testen mehrerer asynchroner Operationen
    test('Sequenzielle asynchrone Operationen', () async {
      var data1 = await fetchUserData(1);
      var data2 = await fetchUserData(2);

      expect(data1, isNot(equals(data2)));
      expect(data1, contains('1'));
      expect(data2, contains('2'));
    });
  });
}
00:00 +4: All tests passed!

Häufige Befehle zum Ausführen von Tests

BefehlFunktionen
dart testAlle Tests ausführen
dart test test/calculator_test.dartEine bestimmte Testdatei ausführen
dart test --name="add"Nur Tests ausführen, deren Namen "add" enthalten
dart test --concurrency=4Tests parallel ausführen (zur Beschleunigung)
dart test --reporter=expandedAusführlicher Ausgabemodus
dart test --coverage=coverageTestabdeckungsdaten generieren

Best Practices für Tests

ÜbungBeschreibung
Jeder Test sollte nur eine Sache prüfenEin test() entspricht einem Verhalten, wodurch Fehler schneller lokalisiert werden können.
Testnamen beschreiben Verhalten, nicht Implementierung"add() addiert zwei positive Zahlen" ist besser als "add() test".
AAA-MusterArrangieren (Einrichten) → Handeln (Ausführen) → Zusichern (Überprüfen)
Schreibe zuerst fehlschlagende TestsBestätige, dass der Test Fehler erkennen kann, und schreibe dann Code, damit er besteht.
Testen von GrenzbedingungenNull (null), Null (0), negative Zahlen, sehr große Werte, sehr kleine Werte.
Tests voneinander unabhängig haltenDas Ergebnis eines Tests sollte einen anderen Test nicht beeinflussen

Tests sind keine Last, sondern eine Versicherung. Je mehr Tests du schreibst, desto sicherer bist du beim Refactoring. Wenn ein Test fehlschlägt, musst du nicht raten, was schiefgelaufen ist – der Test sagt es dir genau.


Kapitelzusammenfassung

Dieses Kapitel führt in den vollständigen Workflow von Dart-Unit-Tests ein: Installation des test-Pakets, Schreiben von test()-Testfällen, Verwenden von expect()-Assertions, Organisieren mit group(), Verwalten der Umgebung mit setUp/tearDown und asynchrones Testen.

Unit-Testing ist ein Pflichtkurs für professionelle Entwickler; die Gewohnheit, Tests zu schreiben, wird deine Codequalität kontinuierlich verbessern.


Zusammenfassung des vollständigen Tutorials

Herzlichen Glückwunsch zum Abschluss deiner Einführung in die Programmiersprache Dart!

Lass uns den Lernpfad dieser 21 Kapitel durchgehen:

PhaseKapitelKerninhalt
1. Grundlagen für den EinstiegKapitel 1-4Dart-Überblick, Einrichtung der Umgebung, erstes Programm, grundlegende Syntax.
2. Kerntypen und KontrollflussKapitel 5-8Variablen und Datentypen, Operatoren, Kontrollfluss, Sammlungen.
3. Funktionen und objektorientierte ProgrammierungKapitel 9-13Funktionen, Klassen und Objekte, Vererbung und Polymorphismus, Schnittstellen und Mixin, Generics.
4. Erweiterte FeaturesKapitel 14-17Enums und Symbole, Ausnahmebehandlung, Paket- und Bibliotheksverwaltung, typedef.
5. Asynchrone Programmierung und TestenKapitel 18-21Asynchrone Programmierung, Streams, Nebenläufigkeit und Isolate, Unit-Testing.

Wenn Sie all diese Inhalte gemeistert haben, sind Sie bereits in der Lage, Dart für die tägliche Entwicklung zu verwenden.

Nächster Schritt: Sie können tiefer eintauchen.Flutter FrameworkDart-Kenntnisse für die UI-Entwicklung auf Mobilgeräten und im Web anwenden.

Weitere Erweiterungen