Im Spiel: was man nicht testen kann
Optional. Diese Seite ist ein Zusatzangebot am Ende des Kapitels. Sie führt nichts Neues ein, und weder der Rückblick noch die späteren Kapitel setzen sie voraus.
Ein Spiel ist der ehrlichste Prüfstein für dieses Kapitel. Denn ein Spiel testet man normalerweise, indem man es spielt – und genau das ist kein Testen, sondern Hoffen.
Scratch for Java in fünf Minuten
Diese Seite macht einen Entwurf mit der Grafikbibliothek Scratch for Java sichtbar. Du musst sie nicht kennen – hier steht alles, was du dafür brauchst. Geladen wird sie über libraries="scratch"; import-Zeilen gibt es nicht.
| Begriff | Was er bedeutet |
|---|---|
Stage |
die Bühne, 480 × 360 Pixel. Der Punkt (0, 0) liegt in der Mitte, die y-Achse zeigt nach oben. Man schreibt eine eigene Klasse, die von Stage erbt. |
Sprite |
eine Figur auf der Bühne. Auch dafür schreibt man eigene Klassen, die von Sprite erben. |
addCostume("coin_gold") |
gibt einer Figur ihr Bild. Die Bibliothek bringt über 1700 fertige Kostüme mit. |
this.add(objekt) |
fügt eine Figur oder einen Text der Bühne hinzu. Was nicht hinzugefügt wurde, sieht man nicht. |
run() |
wird für jedes Objekt etwa 60-mal pro Sekunde aufgerufen. Alles Fortlaufende steht hier drin. |
getTouchingSprite(Muenze.class) |
liefert das berührte Objekt oder null, wenn gerade keins berührt wird. Das null muss man immer abfragen. |
Dazu isKeyPressed(KeyCode.RIGHT) für die Tastatur, changeX(4) zum Bewegen, remove() zum Entfernen und new Text(text, x, y, breite) mit showText(...) für die Anzeige.
Zum Nachlesen: Die erste Grafik erklärt Bühne und Koordinatensystem, Eigene Sprites die eigenen Figurenklassen mit run(). Alle Methoden auf einen Blick: Referenz.
Woran es hakt
Hier ist eine Punkteregel, wie sie in jedem zweiten Spiel steht: Wer mehrere Gegenstände schnell hintereinander einsammelt, bekommt einen Kombo-Bonus. Ab dem dritten Treffer in Folge zählt jeder Treffer doppelt, ab dem sechsten dreifach. Eine Falle setzt die Kombo zurück.
In der ersten Fassung steht diese Regel dort, wo sie gebraucht wird – mitten im Sprite:
public class Muenze extends Spielobjekt {
public void beruehrtVon(Spieler pSpieler) {
Spiel s = this.spiel();
s.setKombo(s.getKombo() + 1);
int faktor = 1;
if (s.getKombo() >= 6) {
faktor = 3;
} else if (s.getKombo() >= 3) {
faktor = 2;
}
s.zaehle(10 * faktor);
this.remove();
}
}
Diese Regel hat mindestens vier Fälle und zwei Grenzen – genau die Sorte Code, die man testen will.
Aber wie? beruehrtVon braucht einen Spieler, der Spieler braucht eine Bühne, die Bühne braucht ein Fenster. Um zu prüfen, ob der dritte Treffer wirklich doppelt zählt, müsste ein Test ein ganzes Spiel starten und eine Figur steuern.
Das ist der eigentliche Befund: Die Regel ist nicht zu kompliziert zum Testen – sie steht nur am falschen Ort.
Die Trennung
Die Punkteregel braucht keine Grafik. Sie braucht nur zu wissen, was passiert ist. Also bekommt sie eine eigene Klasse ohne jede Verbindung zur Bühne:
Eine Klasse, die nur rechnet und nichts anzeigt, heißt Logik. Sie ist ohne Umgebung lauffähig und deshalb ohne Umgebung prüfbar.
Der Sprite wird dadurch dumm – und das ist gewollt: Er meldet nur noch, dass etwas eingesammelt wurde, und fragt nicht mehr, was das bedeutet.
Im Programmierbereich unten stehen beide Teile nebeneinander: Punktestand als reine Logik und PunktestandTest mit den Tests dazu. Starte die Tests über den Reiter Testrunner unter dem Editor.
Sechs Tests, in einer Sekunde durchgelaufen, ohne dass jemand eine Taste drücken musste. Dieselbe Sicherheit durch Spielen zu bekommen, würde Minuten dauern – und man müsste die Kombo von 5 auf 6 jedes Mal von Hand treffen.
Die Grenzen sind der Kern: 2 → 3 und 5 → 6. Genau dort stehen die >=-Vergleiche, und genau dort passieren die Fehler. Ein Test, der nur „drei Treffer geben 40 Punkte" prüft, findet einen vertauschten Vergleich nicht.
Teste immer beide Seiten einer Grenze – den letzten Fall davor und den ersten danach.
Aufgabe 1: Fehler einbauen und finden
a) Ändere in faktor() das erste >= in ein >. Welche Tests schlagen fehl, welche nicht? Was sagt dir das über die, die nicht fehlschlagen?
b) Vertausche die beiden if-Blöcke in faktor(). Schlägt ein Test fehl? Begründe.
c) Schreibe einen Test für den Fall, dass fehler() vor dem ersten Treffer kommt.
d) treffer(0) – ein Gegenstand ohne Wert. Erhöht er die Kombo? Entscheide, was richtig ist, und schreibe zuerst den Test dafür.
Der zweite Teil: was das Spiel kostet
Die Kollisionsprüfung sieht harmlos aus:
Spielobjekt getroffen = this.getTouchingSprite(Spielobjekt.class);
Eine Zeile, ein Aufruf. Nur muss die Bibliothek dafür jedes Spielobjekt auf der Bühne prüfen. Bei einem Spieler und n Gegenständen sind das n Prüfungen pro Bild.
Solange nur der Spieler prüft, wächst der Aufwand linear. Sobald auch Gegner untereinander prüfen, prüft jeder gegen jeden: n · (n−1) / 2 Paare – quadratisch.
Aufgabe 2: den Aufwand abschätzen
a) 20 Gegenstände, 60 Bilder pro Sekunde, nur der Spieler prüft. Wie viele Prüfungen pro Sekunde?
b) Dieselbe Rechnung, aber jeder gegen jeden bei 20 Objekten. Und bei 200?
c) Ab welcher Zahl von Objekten wird das zum Problem, wenn der Rechner etwa eine Million Prüfungen pro Sekunde schafft? Rechne, statt zu schätzen.
d) Ein gängiger Ausweg: Die Bühne wird in Kacheln von 50 × 50 Pixeln eingeteilt, und geprüft wird nur gegen Objekte in derselben und den acht angrenzenden Kacheln. Wie ändert sich der Aufwand – und welchen Preis zahlt man dafür?
Und ohne Spiel?
| Im Spiel | Dieselbe Einsicht woanders |
|---|---|
| Punkteregel aus dem Sprite herauslösen | Geschäftslogik von der Benutzeroberfläche trennen |
| Grenzen 2→3 und 5→6 prüfen | Notengrenzen, Rabattstufen, Altersgrenzen |
| „das teste ich, indem ich es spiele" | „das teste ich, indem ich es benutze" |
| jeder gegen jeden | jeder Datensatz gegen jeden anderen beim Abgleich |
Die Einsicht dieser Seite ist keine über Spiele: Schlecht testbarer Code ist fast immer schlecht entworfener Code. Wenn ein Test unmöglich erscheint, liegt es selten am Test.