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):
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):
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
| Matcher | Zweck | Beispiel |
|---|---|---|
| equals(expected) | Prüft, ob Werte gleich sind | expect(result, equals(42)) |
| isTrue / isFalse | Überprüft boolesche Werte | expect(flag, isTrue) |
| isNull / isNotNull | Null überprüfen | expect(value, isNull) |
| contains) | Enthält ein Element (Liste) oder eine Teilzeichenfolge (String). | expect(list, c('a')) |
| isA | Typen überprüfen | expect(obj, isA |
| throws | Überprüfen, ob eine Ausnahme ausgelöst wurde | expect(() => f(), throwsException) |
| isNotEmpty | nicht null | expect(list, isNotEmpty) |
| hasLength(n) | Länge überprüfen | expect(list, hasLength(3)) |
| greaterThan(n) | größer als | expect(score, greaterThan(60)) |
| closeTo(value, delta) | Gleitkommazahlen ungefähr gleich | expect(3.14, closeTo(3.1, 0.1)) |
Beispiel
Praktische Anwendungen verschiedener Matcher:
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
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
// 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
// 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
// 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
| Befehl | Funktionen |
|---|---|
| dart test | Alle Tests ausführen |
| dart test test/calculator_test.dart | Eine bestimmte Testdatei ausführen |
| dart test --name="add" | Nur Tests ausführen, deren Namen "add" enthalten |
| dart test --concurrency=4 | Tests parallel ausführen (zur Beschleunigung) |
| dart test --reporter=expanded | Ausführlicher Ausgabemodus |
| dart test --coverage=coverage | Testabdeckungsdaten generieren |
Best Practices für Tests
| Übung | Beschreibung |
|---|---|
| Jeder Test sollte nur eine Sache prüfen | Ein 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-Muster | Arrangieren (Einrichten) → Handeln (Ausführen) → Zusichern (Überprüfen) |
| Schreibe zuerst fehlschlagende Tests | Bestätige, dass der Test Fehler erkennen kann, und schreibe dann Code, damit er besteht. |
| Testen von Grenzbedingungen | Null (null), Null (0), negative Zahlen, sehr große Werte, sehr kleine Werte. |
| Tests voneinander unabhängig halten | Das 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:
| Phase | Kapitel | Kerninhalt |
|---|---|---|
| 1. Grundlagen für den Einstieg | Kapitel 1-4 | Dart-Überblick, Einrichtung der Umgebung, erstes Programm, grundlegende Syntax. |
| 2. Kerntypen und Kontrollfluss | Kapitel 5-8 | Variablen und Datentypen, Operatoren, Kontrollfluss, Sammlungen. |
| 3. Funktionen und objektorientierte Programmierung | Kapitel 9-13 | Funktionen, Klassen und Objekte, Vererbung und Polymorphismus, Schnittstellen und Mixin, Generics. |
| 4. Erweiterte Features | Kapitel 14-17 | Enums und Symbole, Ausnahmebehandlung, Paket- und Bibliotheksverwaltung, typedef. |
| 5. Asynchrone Programmierung und Testen | Kapitel 18-21 | Asynchrone 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