The way to write unit exams in C++ counting on non-code information?

Not too long ago we had a coding dojo with my colleagues the place we have been engaged on the second a part of the Racing Car Katas, known as TextConverter. To sum up the issue, the HtmlTextConverter
class takes a filename, reads the file into reminiscence and converts its content material right into a not-very-sophisticated HTML textual content.
The aim is to check the category and probably refactor it when you discover any good motive for that. In my view, there are many causes to refactor this class. The primary downside is that it does no less than two issues. It 1) reads a file and a pair of) converts its contents to HTML. It’s tough to jot down unit exams for this class as a result of a unit check ought to be quick and ideally mustn’t rely upon issues equivalent to IO or community.
On this case, we clearly rely upon the file system. Nonetheless, it’s attainable to offer a check that works. We are able to create a file and use it within the check. With out having that check, refactoring is just not protected as we wouldn’t know if we broke one thing.
A primary naive strategy
As a primary try, within the check listing, we created a file known as simpleText.txt
with a number of strains in it and wrote this check.
1
2
3
4
5
6
7
TEST(HTMLTextConverter, CorrectHtmlIsGeneratedWithSimpleNonEscapedInput) {
const std::string expectedOutput = R"(line1<br />line2<br />line3<br />)";
std::string filePath { "simplefile.txt" };
HtmlTextConverter converter { filePath };
ASSERT_EQ(expectedOutput, converter.convertToHtml());
}
We didn’t count on it to work, however we wished some quick suggestions. It failed because the content material was not learn in, and the file was not discovered. Oh, by the way in which, the unique code of HtmlTextConverter
doesn’t make any distinction between an empty and a lacking file…
As a subsequent step, we up to date the CMake settings to compile with C++17 and tried to make use of std::filesystem
. We had some surprises with the filesystem API, equivalent to its lack of help for operator+
and the variations between concat
and append
or between operator+=
and operator/=
, however that’s one other story.
1
2
3
4
5
6
7
TEST(HTMLTextConverter, CorrectHtmlIsGeneratedWithSimpleNonEscapedInput) {
const std::string expectedOutput = R"(line1<br />line2<br />line3<br />)";
std::filesystem::path filePath = std::filesystem::current_path().append("simplefile.txt");
HtmlTextConverter converter { filePath };
ASSERT_EQ(expectedOutput, converter.convertToHtml());
}
It nonetheless didn’t work, the output was empty. So we determined to print the trail. What may have gone incorrect? It returned one thing surprising to us:
1
filePath: "/Customers/sandord/private/dev/dojos/Racing-Automotive-Katas/Cpp/cmake-build-script/TextConverter/exams/simplefile.txt"
Oh la la! That’s clearly not the place we created the file! cmake-build-script/
was an surprising component of the trail! Okay, so the unit check was in search of the file within the construct folder, not the place the file we wished to compile initially resided…
We discovered three completely different approaches to resolve this downside.
Use __FILE__
to get the unique path
If you would like the unique path of the file, you should use the __FILE__
preprocessor macro. You don’t get the listing path, however the file path. Which means it’s important to do away with the file title. Fortunately, it’s straightforward to do with std::filesystem::path::remove_filename
.
1
2
3
4
5
6
7
8
TEST(HTMLTextConverter, CorrectHtmlIsGeneratedWithSimpleNonEscapedInput) {
const std::string expectedOutput = R"(line1<br />line2<br />line3<br />)";
std::filesystem::path dirPath = std::filesystem::path(__FILE__).remove_filename();
std::filesystem::path filePath = dirPath /= "simplefile.txt";
HtmlTextConverter converter { filePath };
ASSERT_EQ(expectedOutput, converter.convertToHtml());
}
It has one other draw back as nicely, however we’ll unravel that one later.
Create a world variable from CMake
So we want the trail of the unique file. We all know the worth precisely in CMake. Additionally in CMake, we will create world constants. To be extra exact, with target_compile_definitions
, we will populate the COMPILE_DEFINITIONS
property with a semicolon-separated record of preprocessor definitions utilizing the syntax VAR
or VAR=worth
.
Here’s a approach to resolve our downside. We added this to our CMakeLists.txt
file:
1
2
3
4
5
target_compile_definitions(
HtmlTextConverter_Test_Gmock
PUBLIC
RESOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
)
Then we will use within the exams:
1
2
3
4
5
6
7
TEST(HTMLTextConverter, CorrectHtmlIsGeneratedWithSimpleNonEscapedInput) {
const std::string expectedOutput = R"(line1<br />line2<br />line3<br />)";
std::filesystem::path filePath = std::filesystem::path(RESOURCE_DIR) += "/simplefile.txt";
HtmlTextConverter converter { filePath };
ASSERT_EQ(expectedOutput, converter.convertToHtml());
}
There are a few issues.
First, when you see this check, you’ve got completely no concept the place RESOURCE_DIR
comes from. We may help slightly bit on that downside, by introducing a helper variable someplace in the beginning of the file. One other potential downside is that it’s outdated char array. However with the helper variable, you resolve that downside as nicely.
1
2
3
4
5
6
7
8
9
std::filesystem::path RESOURCES_PATH {RESOURCE_DIR};
TEST(HTMLTextConverter, CorrectHtmlIsGeneratedWithSimpleNonEscapedInput) {
const std::string expectedOutput = R"(line1<br />line2<br />line3<br />)";
std::filesystem::path filePath = RESOURCES_PATH /= "simplefile.txt";
HtmlTextConverter converter { filePath };
ASSERT_EQ(expectedOutput, converter.convertToHtml());
}
A 3rd downside which I already hinted about is that what in case your exams change the file?
You both should
- undo the adjustments. Positive, however how? And the way a lot work is that?
- depend on git to revive the file. Can we assume that the code is used alongside git? Perhaps sure, perhaps no, nonetheless it’s one thing to think about. Let’s say you possibly can depend on Git. How a lot time is that going to take?
- or the most effective could be nonetheless to repeat the file together with code information and simply discard them together with the construct folder. In fact, if the check information are large, that is problematic. However unit exams shouldn’t rely upon enormous check information. Properly, they shouldn’t rely upon textual content information anyhow, proper…?
Copy the sources to the construct file
It’s not within the scope of this text to find dealing with git
from C++ or to undo adjustments on a file, however we’re going to see how one can copy the sources to the construct folder.
Copying information over to the construct folder could be very easy.
1
2
3
configure_file(emptyfile.txt ${CMAKE_CURRENT_BINARY_DIR}/emptyfile.txt COPYONLY)
configure_file(escapedfile.txt ${CMAKE_CURRENT_BINARY_DIR}/escapedfile.txt COPYONLY)
configure_file(simplefile.txt ${CMAKE_CURRENT_BINARY_DIR}/simplefile.txt COPYONLY)
With configure_file
, we copy our sources from the present folder (assuming that they’re in the identical folder because the CMakeLists.txt
file) to the CMAKE_CURRENT_BINARY_DIR
. On this case, we merely copy them, however you have several options. This step is sufficient to run the exams efficiently.
To have a clear answer, we want two extra instructions. With add_custom_target
, we create a goal to signify the copying operation. Then with add_dependencies
, we set up the dependency relationship between this tradition goal and different targets within the construct course of. This separation of considerations permits for higher group and administration of the construct course of in CMake.
It’s price noting that this doesn’t make the textual content information a part of the binary. In case you have a binary to distribute that will depend on textual content information, these information nonetheless should be distributed together with the binary.
1
2
3
4
5
add_custom_target(CopyTextFile ALL DEPENDS
${CMAKE_CURRENT_BINARY_DIR}/emptyfile.txt
${CMAKE_CURRENT_BINARY_DIR}/escapedfile.txt
${CMAKE_CURRENT_BINARY_DIR}/simplefile.txt)
add_dependencies(HtmlTextConverter_Test_Gmock CopyTextFile)
That’s it. On this third answer, we copy over the information every time to the construct folder, so any adjustments completed by the exams are discarded. In addition to the relative path is stored, we will use such helpful constructs as std::filesystem::current_path
in our code:
1
2
3
4
5
6
7
TEST(HTMLTextConverter, CorrectHtmlIsGeneratedWithSimpleNonEscapedInput) {
const std::string expectedOutput = R"(line1<br />line2<br />line3<br />)";
std::filesystem::path filePath = std::filesystem::current_path().append("simplefile.txt");
HtmlTextConverter converter { filePath };
ASSERT_EQ(expectedOutput, converter.convertToHtml());
}
If we now have one other look, this code truly was considered one of our first, naive makes an attempt! And ultimately, the check would solely go the filename with the complete path!
Conclusion
On this article, I shared the other ways we discovered to make a unit check work which will depend on native information. Our first answer makes use of the __FILE__
macro, it’s important to take away the filename from the trail, however it’ll work. The second answer will work so long as you utilize CMake (similar to the third answer), however its readability is just not the most effective and dealing with adjustments to the useful resource information is likely to be difficult.
The third answer solves each the readability and challenge and the issue of file adjustments in a check because it all the time copies the unique information to the construct folder. That copying may take a bit extra time. Hopefully, you don’t depend on enormous information in your unit exams…
How do you resolve this downside? Aside from eradicating the dependency on such information.
Join deeper
Should you appreciated this text, please