I wrote some template function, which helps me with reading input from terminal, doing error handling, validating input.
The full code with template specialisation for strings looks like this:
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;
if(istream)
continue;
istream.clear();
istream.ignore(std::numeric_limits<std::streamsize>::max(), 'n');
ostream << "Reading input failed, please try again!n";
}
istream.ignore(std::numeric_limits<std::streamsize>::max(), 'n');
return input;
}
template <>
inline std::string getInput<std::string>(const std::string& prompt, std::istream& istream, std::ostream& ostream,
std::function<bool(std::string)> isValid) {
std::string textInput;
while(true) {
ostream << prompt << "n>> ";
if(std::getline(istream, textInput) && (!isValid || isValid(textInput)))
break;
if(istream)
continue;
ostream << "You probably entered EOF or exceeded the maximum text input size, please try again!n";
istream.clear();
}
return textInput;
}
Some usage might look like this:
getInput<double>("Enter positve number?", istream, ostream, ()(const double input) {
bool correctInput = (input >= 0);
if(!correctInput)
ostream << "Number can only be greater than 0.n";
return correctInput;
});
So the function works like this in a nutshell:
- Print Question
- Read Input from command line
- If not sucessfull (cin fails), print some generic error message and repeat
- If sucessfull, call lambda validation function (if existent)
- If input is valid return it, otherwise the lambda will print some specific error message and repeat
So what I thought I need to test:
- Reading numbers without lambda function works and fails when expected (cin fails or not)
- Reading numbers with lambda function works and fails when exspected (input valid or not)
- The same just with strings (specialisation)
So in sum 8 TestCases which I structured into two Scenarios: Read numeric input, read non-numeric input.
The whole Code looks like this (uses Catch2 as Unit Test Framework):
using Catch::Matchers::Contains;
using Catch::Matchers::Matches;
SCENARIO("Read numeric input from command line") {
GIVEN("Some input and output stream") {
std::istringstream iss {};
std::ostringstream oss {};
WHEN("calling getInput<int> without valid function entering some non-numeric value") {
iss.str("42");
int input = getInput<int>("Some Question", iss, oss);
THEN("message is printed once and input returned") {
REQUIRE_THAT(oss.str(), Contains("Some Question"));
REQUIRE(input == 42);
}
}
WHEN("calling getInput<int> without valid function entering some non-numeric value") {
iss.str("Stringn42");
int input = getInput<int>("Some Question", iss, oss);
THEN("error message is printed and question is repeated") {
REQUIRE_THAT(
oss.str(),
Matches("(\s\S)*Some Question(\s\S)*please try again(\s\S)*Some Question(\s\S)*"));
}
}
WHEN("calling getInput<int> with valid function entering some valid numeric input") {
iss.str("42");
int input = getInput<int>("Some Question", iss, oss, ()(int value) { return value > 0; });
THEN("message is printed once and input returned") {
REQUIRE_THAT(oss.str(), Contains("Some Question"));
REQUIRE(input == 42);
}
}
WHEN("calling getInput<int> with valid function entering some invalid numeric input") {
iss.str("-20n20");
int input = getInput<int>("Some Question", iss, oss, ()(int value) { return value > 0; });
THEN("the question is repeated") {
REQUIRE_THAT(oss.str(), Matches("(\s\S)*Some Question(\s\S)*Some Question(\s\S)*"));
}
}
}
}
SCENARIO("Read text input from command line") {
GIVEN("Some input and output stream") {
std::istringstream iss {};
std::ostringstream oss {};
WHEN("calling getInput<std::string> without valid function entering some text") {
iss.str("Some input");
std::string input = getInput<std::string>("Some Question", iss, oss);
THEN("message is printed once and input returned") {
REQUIRE_THAT(oss.str(), Contains("Some Question"));
REQUIRE(input == "Some input");
}
}
WHEN("calling getInput<int> without valid function entering some non-numeric value") {
iss.str("StringnString");
iss.setstate(std::ios::failbit);
std::string input = getInput<std::string>("Some Question", iss, oss);
THEN("error message is printed and question is repeated") {
REQUIRE_THAT(
oss.str(),
Matches("(\s\S)*Some Question(\s\S)*please try again(\s\S)*Some Question(\s\S)*"));
}
}
WHEN("calling getInput<int> with valid function entering some valid text input") {
iss.str("Some input");
std::string input = getInput<std::string>("Some Question", iss, oss,
()(std::string value) { return value.length() >= 3; });
THEN("message is printed once and input returned") {
REQUIRE_THAT(oss.str(), Contains("Some Question"));
REQUIRE(input == "Some input");
}
}
WHEN("calling getInput<int> with valid function entering some invalid text input") {
iss.str("anabc");
std::string input = getInput<std::string>("Some Question", iss, oss,
()(std::string value) { return value.length() >= 3; });
THEN("the question is repeated") {
REQUIRE_THAT(oss.str(), Matches("(\s\S)*Some Question(\s\S)*Some Question(\s\S)*"));
}
}
}
}
My ideas how to test it:
- Mock input and output by using stringstreams
- In success cases check that input is correctly (obviously) and that question was in fact asked (Not to sure if one would do that … but I thought this is something I expect from the method, so why not test it)
- In failure cases, check if question was asked twice (to test whether the user is asked again for input) and possibly check if some error message is printed if it’s expected (-> cin fails)
In general I’m happy for any suggestion. But just to list some things / questions in particular, where I would love to get feedback:
- Are my Scenarios / test cases good chosen, i.e. is the structure good
- Can my wording be improved. I don’t mean specific language errors here, but rather if the content is good. So for example that I need fake streams here doesn’t seems so relevant. Mainly because I just need them to make the function testable. Without tests, I wouldn’t need them. But in this case, there would be nothing else that I need in the given part (usually for classes you would need at least the object with some properties set)
- Is the code in the right place, for example I wondered whether the
iss.str()
part shouldn’t be in the “GIVEN” rather as I usually configure mocks there. But the concrete values for mocks depend here on my THEN, which makes it clearer to me to put them inside the THEN. - Are my Asserts any good? I had particular problems to test my failure cases as in these case, the function enters the while loop. So I always had to mock the stream, so it would fail first and then pass, which seemed a little bit weird to me. Furthermore it wasn’t to easy to assert that behaviour then.
As said, if you see other improvements please show me them as well 🙂
Thanks for your valuable feedback!