1
\$\begingroup\$

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("String\n42");

            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("-20\n20");

            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("String\nString");
            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("a\nabc");

            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!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.