Unit Tests
This blog isn't primarily about unit testing as such, but there are several areas where the facilities of modern C++ do assist with writing the tests.
Let's look at building some simple unit tests around the class developed in the last post, VersicleAndResponsePair.
Because this was developed as a refactoring of an existing class which was previously under unit tests, the core functionality has been verified already, and we'll be wanting to test edge cases.
I use Boost's test suite for writing my unit tests. We'll start with a simple test of what happens if you access an instance which has not been set with values.
BOOST_AUTO_TEST_CASE(versicle_and_response_pair_unit_test)
{
BreviaryLib::VersicleAndResponsePair thePair;
BOOST_CHECK_EQUAL(thePair.getValues().size(), 2);
BOOST_CHECK(thePair.getValues()[0].empty());
BOOST_CHECK(thePair.getValues()[1].empty());
}
This passes. We might want to consider whether this is what we want: the underlying array will always have a size of 2, even if the values have never been set. It turns out that the only places where this is used copy the returned span using an STL algorithm, and that that algorithm is copy_if, checking for the values being empty, so this is acceptable. (If it weren't acceptable fixes would be (1) adding an empty() test to the class interface; or returning an empty span if there had been no initialization. Neither one is necessary in this case.)
Let's exercise the copy() function first, as the tests are more limited in variety.
I'm going to skip the detailed coverage of TDD flow (write a failing test first), and of course this isn't true TDD because we're writing the tests after the class was written. I do tend to go failing test -> passing test regardless, but I'll be presenting the tests in a passing state only, unless they uncover concerns or bugs.
For the expected normal behaviour (already exercised indirectly, but should be here for good measure):
std::array<std::string, 2> twoValues{ "Foo"s, "Bar"s };
thePair.copy(twoValues);
BOOST_CHECK_EQUAL(thePair.getValues().size(), 2);
BOOST_CHECK_EQUAL(thePair.getValues()[0], "Foo"s);
BOOST_CHECK_EQUAL(thePair.getValues()[1], "Bar"s);
Right away we can see two newer features which help a lot in keeping unit tests clean. BOOST_CHECK_EQUAL() complains if you try to compare std::strings with C-style strings, so the literal for comparison has to be an explicit std::string. This is much neater with string literals than any other alternatives. Similarly, providing collections which can be initialized in one pass with initialization lists saves space and gains clarity. The use of an array rather than a vector, in this case, makes no practical difference.
With a one-value array we get:
{
BreviaryLib::VersicleAndResponsePair thePair;
std::array<std::string, 1> oneValue{ "Foo"s };
thePair.copy(oneValue);
BOOST_CHECK_EQUAL(thePair.getValues().size(), 2);
BOOST_CHECK(thePair.getValues()[0].empty());
BOOST_CHECK(thePair.getValues()[1].empty());
}
This was behaviour which was explicitly designed in, so it's nice to know that it works: a defective source gets nothing copied in at all.
For three values:
{
BreviaryLib::VersicleAndResponsePair thePair;
std::array<std::string, 3> threeValues{ "Foo"s, "Bar"s, "Baz"s };
thePair.copy(threeValues);
BOOST_CHECK_EQUAL(thePair.getValues().size(), 2);
BOOST_CHECK_EQUAL(thePair.getValues()[0], "Foo"s);
BOOST_CHECK_EQUAL(thePair.getValues()[1], "Bar"s);
}
This was also designed in; it could have behaved like the one-item version and not copied anything, but I considered that given the way in which this gets called it made sense to display as much as possible if something quietly goes wrong, and getting the two values displayed is better diagnostics. What it does indicate is that the protection against copying off the end of the array does work and we don't get undefined behaviour.
Now we get to looking at the XML extraction. First, the straightforward case:
std::string text = R"(<versicle>Turn us again, O Lord God of Hosts.</versicle>
<response>Shew the light of Thy countenance, and we shall be whole.</response>
</ChapterResponses>)";
BreviaryLib::VersicleAndResponsePair thePair;
std::string_view view(text);
TestElement element;
thePair.extract(view, element);
BOOST_CHECK_EQUAL(thePair.getValues()[0], "Turn us again, O Lord God of Hosts."s);
BOOST_CHECK_EQUAL(thePair.getValues()[1], "Shew the light of Thy countenance, and we shall be whole."s);
BOOST_CHECK_EQUAL(view, "\n </ChapterResponses>"sv);
This is entirely as expected; it also points up the great usefulness when working with multiline hardcoded test text of using the new raw strings capability.
There aren't actually many ways of messing this up which won't have this effect:
std::string text = R"(<versicle>Turn us again, O Lord God of Hosts.</versicle>
<versicle>Shew the light of Thy countenance, and we shall be whole.</response>
</ChapterResponses>)";
BreviaryLib::VersicleAndResponsePair thePair;
std::string_view view(text);
TestElement element;
BOOST_CHECK_THROW(thePair.extract(view, element), std::runtime_error);
BOOST_CHECK_EQUAL(thePair.getValues()[0], "Turn us again, O Lord God of Hosts."s);
BOOST_CHECK_EQUAL(thePair.getValues()[1], ""s);
BOOST_CHECK_EQUAL(view, "<versicle>Shew the light of Thy countenance, and we shall be whole.</response>\n </ChapterResponses>"sv);
We get an inconsistent logical (but not physical) state in the object and an remaining view which is not synced up, but that's irrelevant: the exception will unwind the stack on which the object resides in any case.
The only formal errors which can generate a successful extract are to have empty values:
{
std::string text = R"(<versicle></versicle>
<response></response>
</ChapterResponses>)";
BreviaryLib::VersicleAndResponsePair thePair;
std::string_view view(text);
TestElement element;
thePair.extract(view, element);
BOOST_CHECK_EQUAL(thePair.getValues()[0], ""s);
BOOST_CHECK_EQUAL(thePair.getValues()[1], ""s);
BOOST_CHECK_EQUAL(view, "\n </ChapterResponses>"sv);
}
or a single value:
{
std::string text = R"(<versicle></versicle>
<response>Shew the light of Thy countenance, and we shall be whole.</response>
</ChapterResponses>)";
BreviaryLib::VersicleAndResponsePair thePair;
std::string_view view(text);
TestElement element;
thePair.extract(view, element);
BOOST_CHECK_EQUAL(thePair.getValues()[0], ""s);
BOOST_CHECK_EQUAL(thePair.getValues()[1], "Shew the light of Thy countenance, and we shall be whole."s);
BOOST_CHECK_EQUAL(view, "\n </ChapterResponses>"sv);
}
In both cases my view is that allowing the data to be printed, rather than generating a fatal exception is likely to be the more effective diagnostic.
Comments
Post a Comment