Discussion:
Mocking von Dateien in privaten Methoden möglich?
(zu alt für eine Antwort)
Christian H. Kuhn
2017-09-01 12:31:15 UTC
Permalink
Hallo Gemeinde,

Ein Programm verwendet Input von Dateien im Filesystem, deren Name
hardcoded ist. Die Originalfiles sind zu groß, um sie in Tests zu
benutzen. Ich möchte Mocks für diese Dateien verwenden.

_Zweck des Programms:_ Deutscher Schachbund (DSB) und Weltschachverband
(FIDE) veröffentlichen Listen der Wertungszahlen ihrer Spieler. Der DSB
vergibt nur eine Wertung (DWZ), die FIDE deren drei (ELO für Standard,
Rapid und Blitz). Das Turniersystem „Schweizer System“ benutzt
Wertungszahlen zum Erstellen einer Startrangliste. Das in Deutschland
meistverwendete Programm „SwissChess“ ermöglicht die schnelle Übernahme
von Wertungszahlen durch sog. Hintergrunddateien. Die bislang offiziell
veröffentlichten Hintergrunddateien tragen dort nur DWZ und Standard-ELO
ein. Für Rapid- und Blitzturniere wäre eine Hintergrunddatei
wünschenswert, in der die Standard-ELO mit der Rapid- bzw. Blitz-ELO
überschrieben ist, sofern existent. Solche Hintergrunddateien erzeugt
mein Programm. Es funktioniert soweit. Bevor ich den Quellcode
veröffentliche, möchte ich automatische Tests erstellen.

_Struktur:_ Ein bash-Script lädt die Wertungsdateien von DSB und FIDE
herunter, entpackt sie, ruft das JAVA-Programm auf, packt dessen Output
und verschiebt ihn in ein Verzeichnis, das über den Webserver erreichbar
ist, und räumt auf.

Das Java-Programm arbeitet zweistufig: Im ersten Schritt werden die
Wertungsdateien geparst und die Daten in eine Postgresql-Datenbank
geschrieben. Im zweiten Schritt werden die gespeicherten Daten aus der
Datenbank ausgelesen und in der richtigen Formatierung in die
Ausgabedateien geschrieben. Für beide Schritte existiert je eine Klasse.
Die Schnittstelle enthält nur je eine statische Funktion
importData(Connection) bzw. exportData(Connection) mit scope package.
Öffentliche Schnittstelle des package ist nur main() von QFdsbStarter.
Dort wird die JDBC-Connection aus Daten aus einem Ini4j-File erzeugt und
dann importData() und exportData() aufgerufen.

_Problem:_ Die Originaldateien enthalten ca. 90k (DSB) bzw. 330k (FIDE)
Spieler. Ein vollständiger Durchlauf dauert ca. 100 min. Zu Testzwecken
wäre es wünschenswert, wenn statt der Originaldateien Testdateien mit
einigen wenigen testrelevanten Datensätzen verwendet werden könnten.
Natürlich ist es möglich, entsprechend vorbereitete Dateien anstelle der
Originaldaten in den entsprechenden Ordner zu kopieren. Ich meine aber
einmal gelernt zu haben, dass es nicht so wahnsinnig toll ist, wenn
automatisierte Tests solche externen Abhängigkeiten haben. Besser wäre
es, wenn stattdessen Mocks benutzt werden könnten. Die Dateien sind aber
hardcoded und die Dateinamen, die erzeugten Paths und Reader innerhalb
der zu testenden Klasse private bzw. lokal. Gibt es dennoch eine
Möglichkeit, aus der Testklasse heraus Mocks zu erzeugen?

Und hier noch die relevanten Auszüge aus dem Quellcode:

package de.qno.fdsb;

import [...]

final class QFdsbImporter {
private static final String ISO_8859_1 = "ISO-8859-1";

private QFdsbImporter() {
// empty
}

static void importData(final Connection _connection) throws
QFdsbException {
clearTables(_connection);
fillFideTables(_connection);
fillDsbTables(_connection);
}

private static void clearTables(final Connection _connection) throws
QFdsbException {
[...] // some DELETE * FROM statements
}

/**
* writes downloaded FIDE rating lists in database
*/
private static void fillFideTables(final Connection _connection)
throws QFdsbException {
[...] // similar do DSB
}

/**
* Writes DSB rating list in database
*/
private static void fillDsbTables(final Connection _connection)
throws QFdsbException {

final LinkedList<String> dsbFiles = new
LinkedList<String>(Arrays.asList("verbaende", "vereine", "spieler"));
Path file;

for (final String dsbFile : dsbFiles) {
file = QFdsbStarter.home().resolve(dsbFile + ".csv");
try (PreparedStatement insertStatement = _connection
.prepareStatement("INSERT INTO verbaende VALUES (?,
?, ?, ?);");
CSVReader reader = new
CSVReaderBuilder(Files.newBufferedReader(file, Charset.forName(ISO_8859_1)))
.withSkipLines(1).build()) { // _hier ist
die entscheidende Stelle_
switch (dsbFile) {
case "verbaende":
writeDsbVerbaendeToTable(reader, insertStatement);
break;
case "vereine":
writeDsbVereineToTable(reader, insertStatement);
break;
case "spieler":
writeDsbSpielerToTable(reader, insertStatement);
break;
default:
break;
}
} catch (SQLException e) {
throw new QFdsbException(
"Problems while writing verbaende data into DSB
database or with insert statement for DSB "
+ dsbFile + " database",
e);
} catch (IOException e) {
throw new QFdsbException("Problems while reading DSB " +
dsbFile + " data file", e);
}
}
}
[...]
}

TIA
QNo
Patrick Roemer
2017-09-01 18:39:36 UTC
Permalink
Post by Christian H. Kuhn
_Problem:_ Die Originaldateien enthalten ca. 90k (DSB) bzw. 330k (FIDE)
Spieler. Ein vollständiger Durchlauf dauert ca. 100 min. Zu Testzwecken
wäre es wünschenswert, wenn statt der Originaldateien Testdateien mit
einigen wenigen testrelevanten Datensätzen verwendet werden könnten.
Natürlich ist es möglich, entsprechend vorbereitete Dateien anstelle der
Originaldaten in den entsprechenden Ordner zu kopieren. Ich meine aber
einmal gelernt zu haben, dass es nicht so wahnsinnig toll ist, wenn
automatisierte Tests solche externen Abhängigkeiten haben. Besser wäre
es, wenn stattdessen Mocks benutzt werden könnten. Die Dateien sind aber
hardcoded und die Dateinamen, die erzeugten Paths und Reader innerhalb
der zu testenden Klasse private bzw. lokal. Gibt es dennoch eine
Möglichkeit, aus der Testklasse heraus Mocks zu erzeugen?
[...]
Post by Christian H. Kuhn
private static void fillDsbTables(final Connection _connection)
throws QFdsbException {
final LinkedList<String> dsbFiles = new
LinkedList<String>(Arrays.asList("verbaende", "vereine", "spieler"));
Path file;
for (final String dsbFile : dsbFiles) {
file = QFdsbStarter.home().resolve(dsbFile + ".csv");
// ...
Post by Christian H. Kuhn
}
}
[...]
}
Das Problem scheint doch schlicht das "Singleton" QFdsbStarter.home() zu
sein. Reich den Path einfach rein, dann kannst Du für die Tests an
beliebiger Stelle entsprechende kleine Dateien anlegen - ggfs. direkt in
jimfs, dann haben die Tests überhaupt nix mit dem lokalen Dateisystem zu
schaffen.

Viele Grüße,
Patrick
Christian H. Kuhn
2017-09-06 16:19:01 UTC
Permalink
Post by Patrick Roemer
Das Problem scheint doch schlicht das "Singleton" QFdsbStarter.home() zu
sein. Reich den Path einfach rein, dann kannst Du für die Tests an
beliebiger Stelle entsprechende kleine Dateien anlegen - ggfs. direkt in
jimfs, dann haben die Tests überhaupt nix mit dem lokalen Dateisystem zu
schaffen.
Ich habe PowerMockito gefunden, das behauptet, sowas zu können. Ich
werds probieren.

lg
QNo
Patrick Roemer
2017-09-06 18:46:33 UTC
Permalink
Post by Christian H. Kuhn
Post by Patrick Roemer
Das Problem scheint doch schlicht das "Singleton" QFdsbStarter.home() zu
sein. Reich den Path einfach rein, dann kannst Du für die Tests an
beliebiger Stelle entsprechende kleine Dateien anlegen - ggfs. direkt in
jimfs, dann haben die Tests überhaupt nix mit dem lokalen Dateisystem zu
schaffen.
Ich habe PowerMockito gefunden, das behauptet, sowas zu können. Ich
werds probieren.
| And in order to facilitate decent code designs and make the public API
| simple, some desired features have been intentionally left out. In
| some cases, however, these shortcomings force testers to write
| cumbersome code just to make the creation of mocks feasible.
[http://www.baeldung.com/intro-to-powermock]

Ich würde ja eher zu decent code design raten. Mein Vorschlag war, das
Singleton loszuwerden, nicht das Singleton zu mocken. Gib den Pfad zum
Datenverzeichnis einfach als Konstruktor- oder Methodenparameter rein
und fertig.

Viele Grüße,
Patrick
Christian H. Kuhn
2017-09-09 19:02:04 UTC
Permalink
Post by Patrick Roemer
Ich würde ja eher zu decent code design raten. Mein Vorschlag war, das
Singleton loszuwerden, nicht das Singleton zu mocken.
Hatte ich falsch verstanden, sry.
Post by Patrick Roemer
Gib den Pfad zum
Datenverzeichnis einfach als Konstruktor- oder Methodenparameter rein
und fertig.
Hm. Wenn, dann kommt er in die Ini-Datei, in der auch die Zugangsdaten
für die Datenbank liegen. Mal sehen. Bin sowieso gerade massiv am
Refactorn. DBQW (design by quicker writing) funktioniert halt selten ;-)

lg
QNo
Patrick Roemer
2017-09-10 13:03:44 UTC
Permalink
Post by Christian H. Kuhn
Post by Patrick Roemer
Gib den Pfad zum
Datenverzeichnis einfach als Konstruktor- oder Methodenparameter rein
und fertig.
Hm. Wenn, dann kommt er in die Ini-Datei, in der auch die Zugangsdaten
für die Datenbank liegen. Mal sehen.
Es ist (für diese Diskussion) wumpe, wo der Wert "oben in main", wo die
Komponenten zusammengetackert werden, herkommt. Relevant ist, dass
QFdsbImporter nicht auf fest verdrahteten globalen Zustand zugreift,
sondern dass diese Informationen im Konstruktor oder im Methodenaufruf
reingereicht werden sollten. Globaler Zustand via Singleton ist ein
klassisches Designproblem, und das manifestiert sich u.a. in
Schwierigkeiten beim Testen der Komponente in Isolation.

Viele Grüße,
Patrick

Lesen Sie weiter auf narkive:
Loading...