L
Nach meinen ersten Anläufen mit UnitTests und Mocking habe ich jetzt ein paar Methoden, wo ich nicht so richtig weiß wie ich die teste. Mein Gefühl sagt mir, dass ich sie schlecht geschrieben habe und deswegen nicht weiß wie ich sie testen soll
Im wesentlichen geht es um freie Funktionen, welche Teil des Command Line Interface sind. Zum Beispiel den User dazu aufzufordern Daten einzugeben. Und mit diesen Daten wird dann irgendwas gemacht
void createPerson(DatabaseClass database, std::ostream ostream, std::istream istream) {
auto name = getPersonNameInput(ostream, istream);
auto age = getPersonAgeInput();
database.addPerson(Person { name, age });
}
namespace {
std::string getPersonNameInput(std::ostream ostream, std::istream istream) {
while(true) {
ostream << "Enter Person Name";
std::string name;
istream >> name;
if(validName(name))
return name;
ostream << "Name enthält Nummern oder ist aus irgendeinem Grund nicht valide. Bitte neue Eingabe machen";
}
}
}
Bisschen vereinfacht sieht meine Methode, die wesentlich länger ist so aus. Sie sagt dem user was er eingaben soll, nimmt die Eingabe entgegen, validiert diese, lässt den user ggf. wiederholen. Das macht sie für ein paar Eigenschaften. GGf. sind noch ein bisschen mehr Logik drin "Wollen sie noch ein Eintrag für xyz machen" etc.
Dinge, die ich also gerne testen will:
Landet am Ende in der Datenbank das fertige Objekt (das ist ja eig. die Hauptaufgabe)
Funktionieren die Validierungfunktionen
Funktioniert die mehrfache Eingabe bei Falscheingabe
...
Ich habe das ganze schon in verschiedene Funktionen aufgeteilt, die das jeweils machen (und an sich somit eher einfach zu testen sind) und auch die streams als param übergeben (um sie z.B. mit stringstream zu mocken). Ich weiß also per se wie ich die ganzen Unterfunktionen einzelnd Unit Testen könnte und für die Hauptfunktion könnte ich diese dann stubben.
Das Hauptproblem ist nur: Die einzig freie Methode des "public Interface" (also die auch eine Deklaration in einer .h hat) ist die createPerson Methode. Der ganze Rest sind Funktionen, die ich alle in nem anonymen Namespace definiert habe. Das sind ja quasi nur Hilfsfunktionen, die außerhalb nicht gebraucht werden (also in einer Klasse private Funktionen). Und man testet ja eig. nur das public Interface, die "private" Methoden werden implizit im Zweifelsfall mitgetestet. (Edit: Darüber hinaus ist es auch schwierig bis unmöglich freie Funktionen in einem anonymen Namespace zu testen, wenn dann noch zusätzlich Funktionen darauf gemockt werden müssen).
Wenn ich hier also nur "createPerson" testen würde und alles andere explizit zusammen mit ... da hätte ich ja zig Sachen zu assserten.
Sowas ist ja eig. immer ein Hinweis auf ein schlechtes Design z.B. Verletzung des Single Responsibility Prinzips. Habe ich das hier verletzt oder andere Design Fehler? Wie kann ich es besser machen?
Die einzige "Lösung" die mir einfällt ist eben diese ganze Methoden aus dem anonymen Namespace rauszuholen, dann kann man sie auch testen und mocken. Aber da man die Funktionen nirgendswo sonst braucht, scheint mir das falsch.
Manchmal habe ich auch "nützliche" Funktionen in meinem Namespace stehen. Z.B. habe ich mir für mein Anwendungsfall ne Hilfsmethode geschrieben, um Input einzulesen / validieren / error handling zu machen.
template <typename T>
T getInput(const std::string& prompt, std::istream& istream, std::ostream& ostream,
std::function<bool(T)> isValid = nullptr) {
T input;
while(true) {
ostream << prompt << "\n>> ";
if((istream >> input) && (!isValid || isValid(input)))
break;
istream.clear();
istream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
ostream << "Error!\n";
}
istream.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
return input;
}
getInput<double>("How old are you?", istream, ostream, [](const double input) {
bool correctInput = (input >= 0);
if(!correctInput)
std::cerr << "Age cannot be negative.\n";
return correctInput;
});
Diese Funktion ist jetzt im Gegensatz zu anderen Methoden oben eher universeller verwendbar. Praktisch brauche ich sie aber trotzdem nur in meiner CommandLineInterface.cpp ... also ist sie halt in nem anonymen Namespace gelandet
Vlt. packe ich auch zu viel da rein? ... Ich könnte auch ne komplette Datenbank Library in nen anonymen Namespace packagen mit der Begründung: "Brauche die ja nur in dieser einen Datei" ... aber sowas würde man wohl kaum machen.