Entweder habe ich ein Verstaendnisproblem oder eine Bildungsluecke (oder beides) was Tests angeht, und ich braeuchte da mal kurz ein paar klaerende Meinungen. Fangen wir mal mit dem zu testenden Code an, absichtlich etwas konvolut und nicht ganz logisch, aber bleibt mal bei mir:
Nein, die Antwort "aber das kann man vereinfachen" ist nicht gewollt. Das ist Absicht so.
So...jetzt habe ich festgestellt dass es Leute gibt welche fuer den Endpunkt dann solche Unit-Tests schreiben:
Und hier faengt jetzt meine eigentlich Frage an...mit dem habe ich doch nichts getestet, oder?! Der Test ist doch komplett wertfrei, da koennte ich genauso gut ein Code-Diff zur vorherigen Revision machen. Oder bin ich blind und sehe den Wert nicht?!
Ein Test wie ich ihn in dem Fall machen wuerde waere folgendes:
Damit habe ich das Prinzip von "es sollte nur eine Einheit getestet werden" natuerlich gebrochen, aber nur so kann ich, meiner Meinung nach, garantieren dass die Funktion auch das tut was ich von ihr erwarte. Eigentlich ist mir sogar der Storage-Mock ein Dorn im Auge, da waere mir ein
Ein sehr extremes Beispiel dafuer ist eine HTTP-API welche mit Spring umgesetzt wurde, nehmen wir mal an dass
Wenn ich dann noch so einen Unit-Test (der erste oben) habe wo alles gemockt ist, und ich weder ein richtiges
Also was mir gesagt wurde war "Spring starten ist ein Integrations-Tests, Integrations-Tests machen wir nur am Test-System", dass dann die erste Idee war dass wir fuer das Test-System auch nur volle Release-Pakete verwenden duerfen (ein halber Tag um diese zu bauen und mindestens zwei Personen) konnte ich ja noch erfoglreich abwehren. Aber dennoch, ich will doch wissen ob das Ding funktioniert, am besten in dem Moment wo ich meine Aenderungen zur Durchsicht uebergebe (Merge Request mit Pipeline) und nicht erst wenn ich diese auf irgendeinem Test-System (vielleicht noch haendisch) getestet habe. Das ist doch viel zu spaet und zerbrechlich, oder?
Ich verstehe die Argumentation mit den Mocks, ja. Aber ich will doch Black-Box Tests haben in erster Linie, also nicht dass ich Methoden-Aufrufe abfrage sondern dass ich Verhalten validiere.
Der klare Nachteil an meinen Test-Ansatz ist dass wenn
Ein weiteres Thema in der Hinsicht ist das Pruefen von Ausnahmen. Beispiel von oben, ich will validieren dass eine
Damit bekomme ich die Meldung dass der erwartete String nicht stimmte, zum Beispiel dann sowas wie:
Assertion failed, expected <Parameter "criteria" cannot be <null>.> but was <null>.
Gut, damit weisz ich dann dass es nicht funktioniert, aber das ist mir viel zu wenig Information, insbesondere wo ich die Informationen haette. Mein Ansatz war daher immer dieser:
Ja, das liest sich wie ein Backstein (koennte man vereinfachen mit einer Hilfsfunktion), aber so habe ich im Falle dass der Test versagt direkt alle Informationen verfuegbar. Jetzt wurde mir gesagt "Wenn du wissen willst wo die unerwartete Ausnahme herkam, verwende einen Debugger, dass ist nicht die Aufgabe eines Tests." und bei der Aussage bin ich einfach wie der Ochse vorm Scheunentor ausgestiegen. Will man nicht so viele Informationen wie moeglich mit dem Versagen des Tests bekommen? Ich muss doch auf das Ergebnis des Testlaufs blicken und direkt sagen koennen was falsch ist, oder?
Oder bin ich hier komplett neben der Spur und Realitaet und einfach nicht genug gebildeter Programmierer?
Java:
public class ApiEndpoint {
protected Backend backend = null;
public ApiEndpoint(
Backend backend) {
super();
this.backend = backend;
}
public Value getValue(String id, String criteria) {
return backend.getValue(id, criteria);
}
}
public class Backend {
protected Storage storage = null;
public Backend(
Storage storage) {
super();
this.storage = storage;
}
public Value getValue(String id, String criteria) {
// Custom helper only to catch the most glaring mistakes...basically
// like assertions
Require.nonNullNorEmpty("id", id);
Require.nonNull("criteria", criteria);
for (String existingId : storage.listValueIds()) {
if (existingId.equals(id)) {
Value value = loadValue(existingId);
if (performSomeComparisonsOrSuch(value, criteria)) {
return value;
}
}
}
return null;
}
}
public class Storage {
public Storage() {
super();
}
public List<String> listValueIds() {
// Imagine some storage/database/filesystem access here.
return thatIdList;
}
public Value loadValue(String id) {
// Imagine some storage/database/filesystem access here.
return thatValue;
}
}
Nein, die Antwort "aber das kann man vereinfachen" ist nicht gewollt. Das ist Absicht so.
So...jetzt habe ich festgestellt dass es Leute gibt welche fuer den Endpunkt dann solche Unit-Tests schreiben:
Java:
@Test
public void testGetValue() {
Mockito.doReturn(new Value())
.when(backend)
.getValue(Mockito.any(), Mockito.any());
Assertions.assertNotNull(apiEndpoint.getValue("ID", "vegetables");
Mockito.verify(backend)
.getValue(Mockito.any(), Mockito.any());
}
Und hier faengt jetzt meine eigentlich Frage an...mit dem habe ich doch nichts getestet, oder?! Der Test ist doch komplett wertfrei, da koennte ich genauso gut ein Code-Diff zur vorherigen Revision machen. Oder bin ich blind und sehe den Wert nicht?!
Ein Test wie ich ihn in dem Fall machen wuerde waere folgendes:
Java:
// @ExtendWith(MockitoExtension) to catch all mocked but not invoked functions.
// All Mocks created with a failing/throwing default answer to catch non-mocked
// calls.
@Test
public void testGetValue() {
Mockito.doReturn(Arrays.asList("value1", "value2", "value3"))
.when(storage)
.listValueIds();
Mockito.doReturn(new Value("ImagineASaneConstructorHere"))
.when(storage)
.loadValue("value2");
Value returnedValue = apiEndpoint.getValue("value2", "thatCriteria");
Assertions.assertIsThatValueWeWanted(returnedValue);
}
Damit habe ich das Prinzip von "es sollte nur eine Einheit getestet werden" natuerlich gebrochen, aber nur so kann ich, meiner Meinung nach, garantieren dass die Funktion auch das tut was ich von ihr erwarte. Eigentlich ist mir sogar der Storage-Mock ein Dorn im Auge, da waere mir ein
Storage
welches auf definierte Test-Daten geht sogar noch lieber (aber das ist nicht immer so gut moeglich). Jetzt sagt man "dass ist kein Unit-Test, das ist ein Integrations-Test", gut, wird so sein, aber nur mit einem solchen Test kann ich meiner Meinung nach eine brauchbare Aussage ueber die Funktionstuechtigkeit dieser Methode machen.Ein sehr extremes Beispiel dafuer ist eine HTTP-API welche mit Spring umgesetzt wurde, nehmen wir mal an dass
ApiEndpoint
ein Controller
ist:
Java:
@Controller
public class ApiEndpoint {
protected Backend backend = null;
public ApiEndpoint(
Backend backend) {
super();
this.backend = backend;
}
@GetMapping("/module/v1/stuff/value/{ID}")
public Value getValue(
@PathVariable("ID") String id,
@RequestParam("criteria") String criteria) {
return backend.getValue(id, criteria);
}
}
Wenn ich dann noch so einen Unit-Test (der erste oben) habe wo alles gemockt ist, und ich weder ein richtiges
Backend
noch ein Spring starte, kann ich doch gar nichts dazu sagen ob die Funktion auch so funktioniert wie ich erwarte oder nicht bis ich damit auf dem Test-System war. Das ist doch viel zu spaet, oder?Also was mir gesagt wurde war "Spring starten ist ein Integrations-Tests, Integrations-Tests machen wir nur am Test-System", dass dann die erste Idee war dass wir fuer das Test-System auch nur volle Release-Pakete verwenden duerfen (ein halber Tag um diese zu bauen und mindestens zwei Personen) konnte ich ja noch erfoglreich abwehren. Aber dennoch, ich will doch wissen ob das Ding funktioniert, am besten in dem Moment wo ich meine Aenderungen zur Durchsicht uebergebe (Merge Request mit Pipeline) und nicht erst wenn ich diese auf irgendeinem Test-System (vielleicht noch haendisch) getestet habe. Das ist doch viel zu spaet und zerbrechlich, oder?
Ich verstehe die Argumentation mit den Mocks, ja. Aber ich will doch Black-Box Tests haben in erster Linie, also nicht dass ich Methoden-Aufrufe abfrage sondern dass ich Verhalten validiere.
Der klare Nachteil an meinen Test-Ansatz ist dass wenn
Storage
oder Backend
zerbrochen ist, wird so ziemlich alles Rot. Der Vorteil ist dass ich sehe was dann alles zerbricht, hehe, kleiner Scherz.Ein weiteres Thema in der Hinsicht ist das Pruefen von Ausnahmen. Beispiel von oben, ich will validieren dass eine
criteria
von null
eine entsprechende Ausnahme erzeugt. Der Ansatz der mir gesagt wurde ist:
Java:
@Test
public void testGetValueWithNullCriteria() {
IllegalArgumentException thrownException = Assertions.assertThrows(IllegalArgumentException.class, () -> backend.getValue("valid", null));
Assertions.assertEquals("Parameter \"criteria\" cannot be <null>.", thrownException.getMessage());
}
Damit bekomme ich die Meldung dass der erwartete String nicht stimmte, zum Beispiel dann sowas wie:
Assertion failed, expected <Parameter "criteria" cannot be <null>.> but was <null>.
Gut, damit weisz ich dann dass es nicht funktioniert, aber das ist mir viel zu wenig Information, insbesondere wo ich die Informationen haette. Mein Ansatz war daher immer dieser:
Java:
@Test
public void testGetValueWithNullCriteria() {
try {
backend.getValue("valid", null));
Assertions.fail("Call should have failed because of an illegal argument, but did not.");
} catch (IllegalArgumentException e) {
if (!Object.equals(e.getMessage(), "Parameter \"criteria\" cannot be <null>.") {
throw e;
}
}
}
Ja, das liest sich wie ein Backstein (koennte man vereinfachen mit einer Hilfsfunktion), aber so habe ich im Falle dass der Test versagt direkt alle Informationen verfuegbar. Jetzt wurde mir gesagt "Wenn du wissen willst wo die unerwartete Ausnahme herkam, verwende einen Debugger, dass ist nicht die Aufgabe eines Tests." und bei der Aussage bin ich einfach wie der Ochse vorm Scheunentor ausgestiegen. Will man nicht so viele Informationen wie moeglich mit dem Versagen des Tests bekommen? Ich muss doch auf das Ergebnis des Testlaufs blicken und direkt sagen koennen was falsch ist, oder?
Oder bin ich hier komplett neben der Spur und Realitaet und einfach nicht genug gebildeter Programmierer?