The Biggest Problems of Unit Testing With C++
Maybe you’re new to C++. Perhaps you’ve been working with the language for a while and are looking at unit testing for the first time. Either way, unit testing with C++ often seems to have more problems than it should.
There are significant differences between C++ and “modern” languages like Java, C#, and Python. These newer languages, the ones most often associated with unit testing, have different tools. Even their editors, debuggers, and test frameworks are designed differently. But, there’s no reason not to be writing tests for your C++ code.
Let’s discuss some of the obstacles to writing tests for C++. You can conquer these problems and make your code more reliable.
C++ Code Is Complex
C++ code can be complex, and this complexity is precisely why the language attracts some developers. Used effectively, C++ is a fast, flexible, and powerful platform for high-performance applications. Used poorly, it’s a tool for creating untestable legacy code that everyone is afraid to touch lest they lose their weekend.
Compile Vs. Run Time Type Checking
Even though C++ is a statically-typed language, its type system is weak. Type checking is done by the compiler, unlike Java and C#. If you can fool the compiler, you can get away with anything.
With reinterpret_cast we can coerce any pointer type to another with few limitations. With C-style casts, you can make things even worse with an operator that’s harder to spot. So, if you want to write a test for codes that are using these operators, you have to figure out what the original author was trying to do.
The C preprocessor generates code with no type checking at all. Some developers use it to create a private meta language that’s not only not typesafe, but unreadable. Macros can act as an overlaid scripting language that obfuscates the C++ code.
Some consider templates the modern-day replacement for the legacy preprocessor. They’re not wrong, since replacing text macros with compiled code is always a better alternative. But templates replace the preprocessor’s lack of type checking with “duck typing.” This mechanism can still hide your intentions from the compiler and make an end run around type checking.
Java and C# have runtime type checking and provide generics instead of templates. These features can make the code easier to read and retrofit for tests. They don’t mean that all Java and C# applications are easier to troubleshoot or test. For example, a poorly-written (or missing) hashing algorithm can cause many problems with a generic container.
And then there is C++’s manual memory management. C++ 11 removed much of the burden of managing memory with the official adoption of the smart pointer. Boost has been providing similar tools for even longer. But, many legacy developers didn’t get the memo. Moreover, while smart pointers are powerful, they’re not foolproof.
Many “modern” languages lack these features by design. Some developers see this as a strength. Other see it as a weakness.
Java has references instead of pointers, and the C# pointer type has more restrictions than C++’s. Both languages replace manual memory management with garbage collection.
But it’s possible to leak a reference, and even though references are not pointers, Java still has a NullPointerException. Go figure.
Any code can be complicated, and this is hardly a reason to eschew unit testing. A developer can still abuse Java exceptions to the point of being a substitute for goto. An eager functional programmer can make streams inscrutable.
So, while the creators of Java and C# designed them to reduce complexity, it’s still possible to write untestable code. Don’t blame the language.
Write tests now, and use them to increase your knowledge of that complicated legacy system. The tests will help you unravel the design or create a replacement.
The C++ build process is more involved and more time-consuming than for other languages. The C++ build process has two steps: compiling and linking. That difference alone is significant. But, the time to compile code that makes heavy use of templates and can add even more time.
Add to this how many teams structure their large projects. They create a single target, and that is the product of many other builds. Finally, after those targets finish their builds, the system generates a set of executables. One of them is the test binary.
Toss in build targets for a few different platforms, and a complete build cycle can take many minutes, or even hours. This more involved compiling and linking process slows the code/build/test cycle to a crawl.
So, if tests are difficult to run, developers will run them infrequently. They may even ignore them. Eventually, they’re forgotten.
The solution to this problem isn’t easy, but it’s better than not running tests at all. Break the application down into independent components. Create dynamically-linked libraries and built and test them in isolation.
This is a lot of work, and it may feel like that time could be better spent writing new code. But working with a slow build process that everybody hates is an obstacle. It hampers many aspects of the development process, especially tests.
C++’s differing architecture has more disadvantages than the potential to slow the development process to a crawl.
Interpreted languages, including those that run in virtual machines, are easier to mock and fake. Mocking libraries for these languages have access to private class members. They can also mock concrete classes and functions.
Many C++ mocking libraries lack this ability and are limited to mocking only public and virtual class methods. Instead of mocking concrete methods or classes, they expect you to design your code for testing. Legacy code is often impossible to test without either changing it or writing new code just for tests.
But this isn’t a universal problem with C++ mocking frameworks. Isolator++ addresses these limitations and offers the same level of functionality for its C++ version as it does for it’s .NET edition.
Many Resources Are for Other Languages
A significant problem unit testing C++ is that most of the online resources for writing tests are geared toward other languages. Go to your favorite search engine and enter a query for a topic about testing, and most of the articles you find are for Java, Ruby, or C#.
While unit testing has been around for a long time, the modern implementation we’re most familiar with came to prominence with Extreme Programming (XP) and its cousin, Test-driven development (TDD). We usually associate these movements with Java and Ruby, even though we can apply the core concepts to other languages.
But the core concepts behind unit testing apply to any language. If you’re up to writing clean C++ code, you can use the advice in an article about testing with .NET to your code too.
Testing objects and functions in isolation is a universal concept. Writing tests with Arrange, Act, and Assert, is possible with any language, not only Java and C#. There’s no reason to let the dearth of C++ unit testing resources stop you from testing your code.
No More Excuses
C++ is a powerful language, but as the man in the funny red and blue suit says, with great power comes great responsibility. There is no excuse for writing or maintaining code without tests.
Typemock’s Isolator++ is designed to make adding tests to your C++ code easy, regardless of whether it’s new code that you’ve written today or legacy code from years ago. It can isolate any class for testing, regardless of how its dependencies are designed.
Download an evaluation copy today, and leave your biggest problems with unit testing C++ behind.