Unittest - flutter-tutorial-de/dart-programming GitHub Wiki

Table of Contents

Motivation

Fehlerarmut

Programme enthalten Fehler, sobald sie aus mehr als wenigen Zeilen bestehen.

Der Grund dafür ist, dass Software im Wesentlichen aus Verzweigungen besteht. Jede Verzweigung verdoppelt die Anzahl der Zustände, die das Programm haben kann. Bei 10 Verzweigungen sind das 1024 Zustände, bei 20 Verzweigungen 1.048.576, also rund eine Million!

Kein Mensch kann ein Million Zustände überblicken: Wenn jeder Zustand mit 35 Sekunden im Kopf auf Richtigkeit geprüft wird, dauert das Überdenken einer Million Zustände ein Jahr mit 24 Arbeitsstunden pro Tag! Zwar hat ein guter Programmierer die Richtigkeit von - sagen wir mal - 90% der Zustände im Blick, Fehlerfreiheit erfordern aber volle 100.0 %!

Also was tun: Wir legen Tests von den kleinsten Einheiten eines Programms an, die sich sinnvoll testen lassen. Ein solcher Test heißt Unittest. In einem solchen Unittest versuchen wir, alle Zustände einer "kleinen" Einheit auf Richtigkeit zu überprüfen.

Anmerkung: Eine weitere Testkategorie ist der Integrationstest, der das Zusammenspiel von "kleinen" Einheiten überprüft.

Kleine "sinnvolle" Einheiten sind:

  • Funktionen
  • Klassen

Restrukturierung (Refactoring)

Aktiv genutzte Software unterliegt Änderungsbedarf:

  • Es werden mehr Funktionalitäten gewünscht.
  • Es zeigen sich Fehler.
  • Äußere Bedingungen ändern sich, z. B. eine verwendete Bibliothek ändert die Schnittstelle, ein Sprachmittel der Programmiersprache wird als "veraltet" (englisch deprecated) gekennzeichnet etc.
In solchen Fällen kann es sinnvoll sein, nicht nur an wenigen Stellen kleine Änderungen vorzunehmen, sondern die Grundstruktur des Programmcode zu ändern, was bedeutet, dass viele Stellen betroffen sind. Diesen Vorgang nennt man Restrukturierung, englisch Refactoring.

Hier sind Tests Gold wert: Jede Situation, in der ein Test die Richtigkeit nachweist, wird sich nach der Änderung wieder richtig verhalten. Werden also alle "Standardsituationen" mit jeweils einem Test abgebildet, so ist nachgewiesen, dass diese Standardsituationen bei der Auslieferung der neuen Software richtig behandelt werden.

Es muss also unser Ziel sein, möglichst viele Situationen mit Tests abzudecken. Es kann dann immer noch sein, dass Fehler im Programmcode enthalten sind, aber eine Softwareänderung wird nicht die gesamte Produktion eines Unternehmens lahmlegen!

Unittests in Dart

Dart liefert ein Paket mit, das das Erstellen von Unittests sehr einfach gestaltet.

Beispiel

Wir brauchen eine Funktion, die für einen gegebenen String feststellt, welchen Datentyp dieser String hat: eine Ganzzahl, eine Gleitpunktzahl, ein Datum, ein Boolwert oder ein String. Wir definieren Folgendes in der Datei scanner.dart:

enum DataType { undef, bool, int, float, date, string }
DataType typeOf(String string) {
  DataType rc;
  if (string == null){
    rc = DataType.undef;
  } else if (string.toLowerCase() == 'true' || string.toLowerCase() == 'false') {
    rc = DataType.bool;
  } else if (int.tryParse(string) != null) {
    rc = DataType.int;
  } else if (double.tryParse(string) != null) {
    rc = DataType.float;
  } else {
    rc = DataType.string;
  }
  return rc;
}
Dazu bauen wir einen Unittest:
import 'package:test/test.dart';
import 'scanner.dart';
void main(){
  group('typeOf()', (){
    test('typeOf-null', (){
      expect(typeOf(null), equals(DataType.undef));
    )};
    test('typeOf-bool', (){
      expect(typeOf('true'), equals(DataType.bool));
      expect(typeOf('false'), equals(DataType.bool));
      expect(typeOf('True'), equals(DataType.bool));
      expect(typeOf('FALSE'), equals(DataType.bool));
      expect(typeOf(' true'), isNot(DataType.bool));
      expect(typeOf('true!'), isNot(DataType.bool));
    });
    test('typeOf-int', (){
      expect(typeOf('10'), equals(DataType.int));
      expect(typeOf('-32429'), equals(DataType.int));
      expect(typeOf('0'), equals(DataType.int));
      expect(typeOf('00'), equals(DataType.int));
    });
    test('typeOf-float', (){
      expect(typeOf('1.32'), equals(DataType.float));
      expect(typeOf('0.0'), equals(DataType.float));
      expect(typeOf('-33.04'), equals(DataType.float));
      expect(typeOf('+1.3E+13'), equals(DataType.float));
      // Mehr als 2**64 ~ 10E20
      expect(typeOf('12345678901234567890'), equals(DataType.float));
    });
    test('typeOf-string', (){
      expect(typeOf('0, 4'), equals(DataType.string));
      expect(typeOf('True!'), equals(DataType.string));
      expect(typeOf(''), equals(DataType.string));
    });
  });
}
  • import 'package:test/test.dart'; Wir wollen das Paket test nutzen.
  • import 'scanner.dart'; Damit haben wir Zugriff auf die Funktion typeOf(), die in scanner.dart definiert ist.
  • group('typeOf()', (){ ... });<code> Das Paket <code>test bietet eine Funktion group() an:
    • erster Parameter ist ein String, der eine Kurzbeschreibung der Gruppe liefert.
    • es folgt ein Parameter mit einer Callbackfunktion: Diese beinhaltet die eigentlichen Tests.
  • test('typeOf-bool', (){ ... }); Das Paket test bietet eine Funktion test() an:
    • erster Parameter ist ein String, der eine Kurzbeschreibung des Tests liefert.
    • es folgt ein Parameter mit einer Callbackfunktion: Diese beinhaltet den eigentlichen Test.
  • expect(typeOf('true'), equals(DataType.bool)); Das Paket test liefert diese Funktion mit.
    • Sie darf nur innerhalb der Funktion test() aufgerufen werden.
    • Erster Parameter ist der zu testende Ausdruck, zweiter Parameter der erwartete Wert.
    • Für den erwarteten Wert gibt es verschiedene Möglichkeiten, hier ein paar zur Auswahl:
      • equals() der Wert muss gleich sein.
      • isNull der Wert muss null sein.
      • isPositiv der Wert darf nicht negativ oder 0 sein.
      • isNot(equals(expected)) Ungleichheit prüfen, genauer gesagt eine Boolsche Negierung der Bedingung im Argument.
      • startsWith() der Wert muss mit dem erwarteten Wert beginnen.
      • Dokumentation
  • expect(typeOf(' true'), isNot(DataType.bool)); Wir testen nicht nur das Eintreten eines Zustands, sondern prüfen gleichzeitig "Fehleingaben": Ein Boolscher Wert liegt nicht vor, wenn zusätzliche Zeichen vor oder nach dem Schlüsselwort liegen.
Hinweis: die Funktion equals() kann weggelassen werden, genauer gesagt mit dem Argument ersetzt werden:
expect(2 ~/ 3, equals(0));
ist das gleiche wie:
expect(2 ~/ 3, 0);
Vorgehensweise:
  • Erkenne: Die Schnittstelle der Funktion ist der Parameter string und der Rückgabewert.
  • Klassifiziere die Schnittstelle in Teilbereiche:
    • Es gibt genau 6 verschieden Rückgabewerte.
    • Es muss also mindestens 6 Tests geben.
    • Für den Rückgabewert DataType.undef gibt es nur eine Eingabemöglichkeit, die formulieren wir im ersten Test.
    • Für den Rückgabewert DataType.bool gibt es mehr Möglichkeiten, mindestens zwei (für 'true' und 'false'). Allerdings soll die Groß- und Kleinschreibung keine Rolle spielen, also testen wir auch diese Variation.
    • Für den Rückgabewert DataType.int sind 4 Eingabemöglichkeiten denkbar: eine Zahl, eine Zahl mit Vorzeichen sowie die 0.
    • Gibt es Schwellwerte für einen Parameter, dann sollte bei der Eingabe ein Wert darunter, der Schwellwert selber und ein Wert darüber getestet werden.
    • Grenzwerte prüfen: Bei einem Parameter mit Datentyp String sollte immer geprüft werden, was bei einem Leerstring sowie mit dem Wert null passiert.

Beispiel Schnittstellenänderung

Wir stellen fest: die Funktionalität von typeOf() reicht nicht: Es sollen auch hexadezimale Werte verarbeitet werden.

Wir ändern den Programmcode wie folgt:

...
 } else if (int.tryParse(string) != null 
     || string.startsWith('0x') && int.tryParse(string.substring(2), radix: 16) != null) {
Im Unittest ergänzen wir die neue Situation:
...
    test('typeOf-int-hex', (){
      expect(typeOf('0x123'), equals(DataType.int));
      expect(typeOf('0xabcdef'), equals(DataType.int));
      expect(typeOf('0xABCDEF012'), equals(DataType.int));
      expect(typeOf('0xg'), isNot(DataType.int));
      expect(typeOf('0x'), isNot(DataType.int));
    });
...
Beim Schreiben des Tests fällt mir auf, dass Vorzeichen hier nicht berücksichtigt werden. Ich durchdenke die Situation eines Parsers und stelle fest, dass das Vorzeichen als Operator aufgefasst und damit an anderer Stelle berücksichtigt wird. Ich ergänze diesen Sachverhalt in der Dokumentation und stelle erfreut fest, dass mir die Fokussierung auf Testsituationen Klarheit in deren Architektur gebracht hat!

Fazit

Unittests sind wichtig und auf lange Sicht zeitsparend. Ziel sollte es sein, jede kleine Einheit mit solchen Tests abzudecken.

Das Schreiben eines Tests mit "Abklopfen" der Möglichkeiten der Schnittstelle bringt oft Schwachstellen im Algorithmus oder in der Implementierung zum Vorschein.

⚠️ **GitHub.com Fallback** ⚠️