Wer einen Emulator entwickelt, kommt an automatisierten Tests zur Vermeidung von Regressionen kaum vorbei. Eine gängige Herangehensweise sind Unittests. Sie prüfen einzelne Teile des Codes isoliert und müssen deshalb speziell auf den jeweiligen Emulator zugeschnitten werden. Ergänzend dazu verifizieren Integrationstests das Verhalten des Emulators als Ganzes.
Test-ROMs als Integrationstests
Eine besonders interessante Form von Integrationstests sind Test-ROMs. Sie laufen direkt auf der emulierten Plattform und überprüfen gezielt deren Verhalten. Man könnte sagen, sie testen die Hardware „von innen heraus“.
Ein Game Boy Test-ROM könnte zum Beispiel:
- das Timer-Verhalten untersuchen,
indem es den genauen Taktzyklus abpasst,
der das
TIMA-Register erhöht. - die PPU-Mode-3-Dauer bei unterschiedlichen
SCX-, Window- und Sprite-Konstellationen prüfen. - die Auswirkungen des
DIV-Registers auf die APU nachweisen.
Bekannte Test Suites
Es gibt bereits ganze Sammlungen von Game Boy Test-ROMs, sogenannte Test-Suites. Eine der bekanntesten ist die Mooneye Test Suite, entwickelt von Joonas Javanainen (Gekkio). Auch Lior Halphons SameSuite und Matt Curries Mealybug Tearoom Tests sind definitiv einen Blick wert. Daneben existieren noch viele weitere, die ich hier aus Platzgründen nicht alle aufführen kann.
In einem meiner GitHub-Repositories habe ich eine Sammlung von Test-Suites verschiedener Autoren zusammengetragen.
Vor- und Nachteile von Test-ROMs gegenüber Unittests
Die Einstiegshürde ist im Vergleich zu Unittests bei Game Boy Test-ROMs deutlich höher. Sie sind als Integrationstests naturgemäß aufwändiger in der Durchführung: Der komplette Emulator muss gestartet, das Test-ROM geladen und ausgeführt werden, und anschließend ist über eine geeignete Auswertung (z. B. Bildschirm-, Speicher- oder CPU-Register-Abgleich) das Testergebnis zu bestimmen. Auch die Erstellung von Test-ROMs ist komplexer, da ein eigenes Toolset benötigt wird, ROMs separat kompiliert werden müssen und sie in der Regel in Assembler geschrieben sind – keiner besonders gängigen Sprache.
Demgegenüber haben Test-ROMs allerdings auch klare Vorteile:
- Sie sind unabhängig von der Implementierung des Emulators. Jeder Emulator der jeweiligen Plattform kann mit ihnen getestet werden.
- Sie sind lauffähig auf Original-Hardware und können dort von jeder Person auf Korrektheit überprüft werden.
Entwicklung von Game Boy Test-ROMs
Für die Entwicklung von Game Boy Test-ROMs wird eine passende Toolchain benötigt. Mein Favorit ist RGBDS in Kombination mit einem Makefile. RGBDS ist speziell auf die Game Boy Entwicklung zugeschnitten und bringt neben Assembler und Linker auch Tools zur Grafik-Konvertierung und Header-Anpassung mit. Kleine Projekte lassen sich mit rgbds-live sogar direkt im Browser entwickeln und ausprobieren.
Bekannte Alternativen zu RGBDS sind WLA DX (Cross-Assembler für verschiedene CPUs) und GBDK (C-Compiler für Game Boy Software).
Während RGBDS und WLA DX Assembler erfordern, unterstützt GBDK auch C. Gerade für Einsteiger in die Game Boy Entwicklung könnte GBDK also der einfachere Weg sein. Allerdings ist bei der Verwendung von C nicht direkt kontrollierbar, welche CPU-Befehle der C-Compiler tatsächlich generiert. Für Test-ROMs, die oft taktgenaues Timing benötigen, ist deshalb Assembler die bessere Wahl.
Ausführung auf echter Hardware
Um eigene Software auf einem echten Game Boy auszuprobieren, braucht es einen passenden Adapter. Aktuell verwende ich den EZ-FLASH Junior, der per Micro-SD-Karte bespielt werden kann. Auch andere Adapter dieser Art sind inzwischen verfügbar. Eine Suche nach „Game Boy Cartridge Adapter“ auf Google oder Amazon liefert passende Ergebnisse.
Fast alle Adapter können mehrere ROMs gleichzeitig bereitstellen. Auf dem Game Boy wird dann beim Start das gewünschte ROM ausgewählt.

Laptop mit Micro SD-Karte, EZ-Flash Junior und Auswahlmenü auf einem Game Boy Color
Ein einfaches Beispiel-Test-ROM
Zur Veranschalichung betrachten wir ein (sehr einfaches) Test-ROM zum
DIV-Timing.
Es überprüft den Taktzyklus der ersten DIV-Inkrementierung nach einem
DIV-Reset und besteht aus drei Teilen:
- Initialisierung von VRAM und BGP
- Durchführung des eigentlichen Tests
- Anzeige des Ergebnisses (✓ / ╳)
Zum Ausprobieren einfach den folgenden Code aus Listing 1 kopieren und
auf rgbds-live ausführen
(die dort vorbefüllte main.asm komplett ersetzen).
Um den Test fehlschlagen zu lassen,
kann z. B. der numerische Wert in Zeile 48 oder 54 verändert werden.
|
|
Listing 1: ein einfaches Beispiel-Test-ROM zum Ausprobieren auf rgbds-live
Implementierung eines Test-Frameworks
Das obige Test-ROM besteht zu großen Teilen aus Initialisierung und Ergebnisanzeige. Die eigentliche Testlogik nimmt nur rund 20 der knapp 100 Zeilen ein. Bei einer ganzen Test-Suite würde sich dieser Boilerplate-Code in jedem einzelnen Test-ROM wiederholen. Es lohnt sich daher, diese Teile in ein Test-Framework auszulagern, das von allen Test-ROMs eingebunden wird.
In der Regel inkludiert ein Test-ROM den Framework-Code vor dem eigenen Test-Code. Das Framework übernimmt Programmstart und Initialisierung und stellt häufig in der Test-Suite genutzte Methoden bereit. Die bereits angesprochenen Test-Suites Mooneye Test Suite, SameSuite und Mealybug Tearoom Tests verwenden diesen Ansatz.
Ergebnis-Auswertung im Test-ROM
Ziehen wir Initialisierung und Ergebnisanzeige aus dem Beispiel-Test-ROM raus in
ein Test-Framework,
bleibt Listing 2 übrig.
Nach der Initialisierung durch das Test-Framework führt das Test-ROM wie gewohnt
seinen Test aus.
Die Ergebnis-Auswertung bleibt ebenfalls weiterhin Teil des Test-ROMs:
Bei einem Fehler wird die Routine TestFail zur Beendigung aufgerufen
(Zeilen 16 und 23),
bei Erfolg TestSuccess
(Zeile 26).
|
|
Listing 2: Code des Beispiel-Test-ROMs mit eingebundenem Test-Framework
Das Test-Framework (Listing 3) stellt neben Programmstart und
Initialisierung die Routinen TestSuccess und TestFail bereit.
Es kümmert sich außerdem um die Anzeige des Ergebnisses.
Dieser Ansatz macht die einzelnen Tests schlanker und leichter wartbar. Gleichzeitig ist das Test-ROM ist weiterhin komplett frei in der Art und Weise der Auswertung und kann bei Bedarf den Test sofort abbrechen („fail fast“).
Die Mooneye Test Suite ist auf diese Weise implementiert.
|
|
Listing 3: das vom Beispiel-Test-ROM verwendete Test-Framework
Auswertung des Ergebnisses im Test-Framework
Unser Beispiel-Test-ROM hat nur zwei Datenpunkte geprüft:
den Wert des DIV-Registers unmittelbar vor und genau während
dessen erster Inkrementierung.
Das ist überschaubar und lässt sich direkt im Test-ROM auswerten.
Doch wenn Tests hunderte oder sogar tausende Datenpunkte erzeugen,
wird ein Abgleich im Test-ROM selbst schnell unhandlich.
Eine Alternative ist es, stattdessen den Abgleich im Test-Framework vorzunehmen. Dafür reserviert das Framework einen Bereich im WRAM. Jedes Test-ROM legt dort seine tatsächlich gemessenen Werte ab und übergibt zusätzlich die erwarteten Ergebnisse. Das Framework vergleicht beides und zeigt anschließend Erfolg oder Misserfolg an.
Vorteil hierbei ist, dass Test-ROMs noch schlanker werden können, da sie sich nicht mehr um die Auswertung kümmern müssen.
Nachteil ist, dass ein Test immer komplett durchlaufen werden muss, bevor ein Ergebnis vorliegt. Ein „fail fast“ ist nicht möglich, da dies eine Auswertung im ROM selbst voraussetzen würde.
So arbeiten etwa SameSuite und Mealybug Tearoom Tests.
Verwendung in automatisierten Integrationstests
Um das Beispiel-Test-ROM als automatisierten Integrationstest auszuführen, muss es in den zu prüfenden Emulator geladen und für eine bestimmte Zeit ausgeführt werden. Abschließend wird das Ergebnis automatisiert überprüft. Damit dies zuverlässig funktioniert, sind zwei offene Punkte zu beantworten:
- Wie lange muss das Beispiel-Test-ROM laufen, bis ein Ergebnis vorliegt?
- Wie wird das Testergebnis automatisiert überprüft, also auf Erfolg oder Misserfolg?
Diese Fragen stellen sich natürlich nicht nur für unser Beispiel-Test-ROM, sondern für jedes Test-ROM.
Aktuelle Konvention
Mooneye Test Suite, SameSuite und Mealybug Tearoom Tests halten sich dazu alle an dieselbe Konvention. Federführend war hier wohl die Mooneye Test Suite, die ursprünglich Teil des Mooneye GB Emulators war.
-
Ein Test gilt als abgeschlossen, sobald der Befehl
ld b, bausgeführt wird. Da dieser Befehl keinen Effekt hat, außer einen Taktzyklus zu konsumieren, wird er für regulären Code nicht verwendet. -
Ein erfolgreicher Test wird durch eine bestimmte Belegung der CPU-Register signalisiert – ein Ausschnitt der Fibonacci-Folge:
- B = 3
- C = 5
- D = 8
- E = 13
- H = 21
- L = 34
Ein Test, der mit anderen Werten abschließt, gilt als nicht erfolgreich.
Unser Beispiel-Test-ROM erfüllt diese Konvention, wenn das Test-Framework wie in Listing 4 angepasst wird.
|
|
Listing 4: anzupassender Ausschnitt des Test-Framework zur Signalisierung des Testergebnisses
Analoge Testergebnisse
Nicht jedes Test-ROM kann sein Ergebnis selbst auswerten. Ein Beispiel ist die Video-Ausgabe: Das Test-ROM kann zwar das Bild erzeugen, aber nicht prüfen, ob es korrekt dargestellt wird.
In solchen Fällen signalisiert das Test-ROM lediglich per ld b, b,
dass der Test abgeschlossen ist.
Die eigentliche Bewertung übernimmt der den Integrationstest ausführende Code.
Dafür wird dem Test-ROM ein Referenz-Screenshot beigelegt,
der mit der tatsächlichen Ausgabe des Emulators verglichen werden muss.
Auch für die Farbwerte in solchen Screenshots gibt es bereits eine Konvention:
- DMG-Screenshots verwenden folgende Farben:
#000000,#555555,#AAAAAA,#FFFFFF - CGB-Screenshots rechnen 15-Bit-CGB-Farben pro R/G/B-Kanal in 8-Bit-Werte um:
(X << 3) | (X >> 2) - „Non-CGB Mode“-Screenshots nutzen diese Farben:
- BGP:
#000000,#0063C6,#7BFF31,#FFFFFF - OBP:
#000000,#943939,#FF8484,#FFFFFF
- BGP:
Fazit
- Test-ROMs sind ideal für Integrationstests von Emulatoren: Sie prüfen das Verhalten der emulierten Hardware und lassen sich sowohl auf Emulatoren als auch auf Original-Hardware ausführen.
- Im Vergleich zu Unittests sind sie zwar aufwändiger in Erstellung und Durchführung, können aber nach einmaliger Implementierung in jedem Emulator wiederverwendet werden.
- Für präzises Timing ist Assembler die bessere Wahl gegenüber C.
- Ein Test-Framework reduziert Boilerplate-Code und erleichtert die Entwicklung größerer Test-Suites erheblich.
- Mit Tools wie RGBDS, WLA DX oder GBDK sowie passenden Adaptern (z. B. EZ-FLASH Junior) lässt sich der gesamte Weg von der Entwicklung bis zum Test auf echter Hardware umsetzen.