Jetzt mal ein etwas größeres Beispiel als FizzBuzz zu Test Driven Development: Finden eines kürzesten Pfads in ungerichteten Graphen.
Das Beispiel findet man öfters mal im Netz, auch mit unterschiedlichen "Lösungswegen", und ich kann jedem nur empfehlen, das mal selbst zu versuchen. (Wobei das eigentlich für jedes Beispiel gilt, das Bowling-Kata und Sortieren eines Arrays sind auch schöne Beispiele.)
Das ganze ist weniger Tutorial und mehr einfach nur ein Beispiel um es mal zu zeigen, bei Fragen aber einfach was sagen
Der ganze Code findet sich im Repo, potentiell geh ich hier auch nicht auf jede Kleinigkeit ein, aber für einen Überblick sollte es trotzdem auch hier reichen.
Erster Testfall
Der erste Testfall ist wieder, wie üblich, einer der "nichts" macht. In diesem Fall ist es das Suchen eines Pfades in einem leeren Graph.
Um den Testfall zu schreiben, muss man sich aber zumindest ein grundsätzliches Format für die Eingabe des Graphen und des Start- und Zielpunkts überlegen. In diesem Fall wieder das einfachst möglichstes: einfach Strings, jeder "Punkt" im Graphen ist einfach ein Buchstabe, und der Graoh selber ist erstmal ein leerer String.
Das ganze als JUnit-Test:
Und direkt auch der Code, der den Test erfüllt:
Zusätzlich zu dem ersten Testfall gibt es noch ein paar weiter, die damit auch direkt abgehandelt sind, z.B. der Weg von A nach Z, wenn nur eine Kante von B nach X existiert.
Mit den ersten Kante legt man dann auch das Format für Kanten fest, in diesem Fall einfach Startknoten, Zielknoten und Länge hintereinander, also z.B. BX1.
Das ganze wieder mit JUnit:
Das nächst einfache ist dann ein Graph mit einer Kante, in der genau diese Kante gesucht wird:
Und der Code, der das erfüllt, ist dann auch wieder nahezu trivial: Wenn der Graph mit der gesuchten Kante anfängt, ist das der gesuchte Pfad
Allerdings sollte der Graph ungerichtet sein, also sollte der gleiche Pfad auch gefunden werden, wenn der Graph nur die Kante "ZA1" enthält.
Also den Testfall noch ergänzen:
Und die Implementation entsprechend anpassen:
Erstes Refactoring
Der Code in
Also schon mal ein kleines Refactoring:
Der Graph besteht ja auch Kanten (beziehungsweise bisher aus genau einer Kante), also könnte man genau diese jetzt als Klasse einführen, und es dem String ein Kanten-Objekt basteln:
Und das ganze dann in
Das Zusammenbauen des Rückgabewerts ist zwar immer noch nicht sonderlich schön, aber zumindest ausreichen – und die Bedingungen sind jetzt schon mal deutlich schöner.
Weiter gehts morgen...
Das Beispiel findet man öfters mal im Netz, auch mit unterschiedlichen "Lösungswegen", und ich kann jedem nur empfehlen, das mal selbst zu versuchen. (Wobei das eigentlich für jedes Beispiel gilt, das Bowling-Kata und Sortieren eines Arrays sind auch schöne Beispiele.)
Das ganze ist weniger Tutorial und mehr einfach nur ein Beispiel um es mal zu zeigen, bei Fragen aber einfach was sagen
Der ganze Code findet sich im Repo, potentiell geh ich hier auch nicht auf jede Kleinigkeit ein, aber für einen Überblick sollte es trotzdem auch hier reichen.
Erster Testfall
Der erste Testfall ist wieder, wie üblich, einer der "nichts" macht. In diesem Fall ist es das Suchen eines Pfades in einem leeren Graph.
Um den Testfall zu schreiben, muss man sich aber zumindest ein grundsätzliches Format für die Eingabe des Graphen und des Start- und Zielpunkts überlegen. In diesem Fall wieder das einfachst möglichstes: einfach Strings, jeder "Punkt" im Graphen ist einfach ein Buchstabe, und der Graoh selber ist erstmal ein leerer String.
Das ganze als JUnit-Test:
Java:
@Test
void shouldFindNoPathInEmptyGraph() {
String emptyGraph = "";
String path = findPath("A", "Z", emptyGraph);
Assertions.assertThat(path).isEmpty();
}
Java:
private String findPath(final String from, final String to, final String graph) {
return "";
}
Mit den ersten Kante legt man dann auch das Format für Kanten fest, in diesem Fall einfach Startknoten, Zielknoten und Länge hintereinander, also z.B. BX1.
Das ganze wieder mit JUnit:
Java:
@ParameterizedTest
@CsvSource({
"A,Z,''", // leerer Graph
"A,Z,BZ1", // einziger Weg hat nicht den gewünschten Pfad
"A,Z,AX1", // einziger Weg hat nicht das gewünschte Ziel
"A,Z,BX1", // einziger Weg hat weder gewünschtes Ziel noch Start
})
void shouldReturnEmptyPathIfNoneExist(final String from, final String to, final String graph) {
String path = findPath(from, to, graph);
Assertions.assertThat(path).isEmpty();
}
Java:
@Test
void shouldReturnPathIfOnlyThisPathExists() {
String graph = "AZ1";
String path = findPath("A", "Z", graph);
Assertions.assertThat(path).isEqualTo("AZ");
}
Java:
private String findPath(final String from, final String to, final String graph) {
if (graph.startsWith(from + to)) {
return from + to;
}
return "";
}
Also den Testfall noch ergänzen:
Java:
@ParameterizedTest
@CsvSource({
"A,Z,AZ1, AZ", // Pfad ist einziger Weg
"A,Z,ZA1, AZ", // Pfad ist umgekehrt vorhanden
})
void shouldReturnPathIfOnlyThisPathExists(final String from, final String to, final String graph, final String expectedPath) {
String path = findPath(from, to, graph);
Assertions.assertThat(path).isEqualTo(expectedPath);
}
Java:
private String findPath(final String from, final String to, final String graph) {
if (graph.startsWith(from + to)) {
return from + to;
}
if (graph.startsWith(to + from)) {
return from + to;
}
return "";
}
Der Code in
findPath
ist allerdings nicht grad sonderlich schön, besonders das graph.startsWith(from + to)
schafft es gar nicht, irgendeine Intention dahinter auszudrücken.Also schon mal ein kleines Refactoring:
Der Graph besteht ja auch Kanten (beziehungsweise bisher aus genau einer Kante), also könnte man genau diese jetzt als Klasse einführen, und es dem String ein Kanten-Objekt basteln:
Java:
record Edge(String from, String to, int length) {}
private Edge parseEdge(final String graph) {
String from = graph.substring(0, 1);
String to = graph.substring(1, 2);
String length = graph.substring(2, 3);
return new Edge(from, to, Integer.parseInt(length));
}
findPath
nutzen:
Java:
private String findPath(final String from, final String to, final String graph) {
Edge edge = parseEdge(graph);
if (edge.from().equals(from) && edge.to().equals(to)) {
return from + to;
}
if (edge.from().equals(to) && edge.to().equals(from)) {
return from + to;
}
return "";
}
Weiter gehts morgen...