Unit Testing With C++: The How and the Why
Unit Testing With C++: The How and the Why
Learn the inner workings of unit testing with C++ and why it's so important.
Join the DZone community and get the full member experience.Join For Free
DevOps involves integrating development, testing, deployment and release cycles into a collaborative process. Learn more about the 4 steps to an effective DevSecOps infrastructure.
Why isn't unit testing with C++ as common as it is with Java, C#, and Python?
I don't know. I was hoping you did.
If you're developing with C++, you should already be writing tests. There's no reason not to, and many reasons why you need to start today.
What Is Unit Testing?
Before I discuss the why and how of unit testing with C++, let's define what we're talking about.
Unit testing means writing code that verifies individual parts, or units, of an application or library. A unit is the smallest testable part of an application. Unit tests assess code in isolation.
In C++, this means writing tests for methods or functions. Tests only examine code within a single object. They don't rely on external resources such as databases, web servers, or message brokers.
For the sake of this discussion, I'll assume that you execute your unit tests as part of a build or, even better, a continuous integration system.
Why Unit Test With C++?
Find Stupid Bugs Early
We all write stupid bugs. We create off-by-one errors, define incorrect constants, mess up an algorithm, or write one of those "what the heck was I thinking?" mistakes every day. We're only human. Even us C++ developers, despite what those C# folks whisper in the lunchroom.
Without unit tests, we don't catch these errors until we get to integration testing or worse, QA. But it's just as likely that we won't find them until later in production.
So, unit tests act as a safety net. By taking the time to write checks for each method or function as we write them, we catch stupid bugs when we create them.
Not all bugs are stupid. Some bugs are quite intelligent. We call them regressions. If you've been around for more than a few years, you've seen one of these.
Your system has been working in production for a long time, but you need to add a new feature or address a deficiency. So you modify the code and roll out a new version, and something else breaks. If you're lucky, it's easy to figure out. If not, cancel your weekend plans.
Your first line of defense against regressions is unit tests. Creating targeted routines that verify discrete behavior, and then running them on every build helps ensure that a new feature doesn't come with a new bug.
Get Early Feedback
Unit testing with C++ gives you early feedback. Sometimes the feedback is that you haven't caused a new regression. Other times it's whether your new code is doing what you think.
Early feedback means confidence. Without unit tests, you don't know if what you're doing works. Writing code that compiles the first time feels great. Writing code that does what you want right away feels even better.
Writing unit tests for code means writing code that can be broken down into discrete units. Testable code is modular because discretely tested units are loosely coupled.
If this reason for unit testing, combined with the previous one, reads like an advertisement for test-driven development (TDD), that's because they are. TDD works and it's a reason to start using unit testing with C++. Agile systems are associated with languages like Java, Ruby, and C#. Is that because of the languages or the practices associated with them?
No. There's no reason you can't implement a rapidly evolving architecture with C++.
Create Built-in Documentation
Raise your hand if you trust comments.
Yeah, I didn't think so. Even your comments can be unintelligible or outright wrong after a few months.
But have you ever peeked at unit tests to see how something works? Do you breathe a sigh of relief when you check out code and find tests? Tests don't lie. Sometimes the best way to figure out how the code works is to see it in action.
How to Unit Test With C++
Okay, so you're sold on unit testing now, right? Then let's get to it.
Pick a Test Runner
Unit testing with C++ requires a test runner. We write tests as functions, and then we link the functions into a binary that the build executes as a test target. So, we need a main function that knows how to run the tests, check the results, and print a report.
There are too many test runners and frameworks for unit testing C++ to list here. I'll list a few of the more common ones.
Visual Studio comes with the Microsoft Unit Testing Framework for C++. You can create a project with this test runner built in and add test classes with a few clicks.
Google Test is the most well-known cross-platform test runner for C++. It's distributed as source code. So, you have to build it for your environment or include it as a vendor dependency with CMake. It comes with a mocking library and, unlike Microsoft's runner, is open-source software.
Pick a Mocking Framework
Now you can run tests. So, you need to write them. The key to writing effective tests is to pick a discrete unit and then verify its functionality in isolation. If a test fails and it's not obvious what caused the failure, the unit under test is not isolated.
Depending on what we are testing, we may need to use a mock. Mocking is creating an object that mimics the behavior of another object. For example, if we are testing an object that interacts with a messaging API, we would mock the messaging connection, rather than write a test that requires connectivity with a messaging broker.
Typemock's Isolator++ is a C++ mocking library for Windows and Linux. With it, you can fake any class or template and any method, without changing your existing code.
The Google Test project bundles Google Mock with the test runner. You can use it to mock C++ classes and templates but the library has limitations that make working with concrete and free functions difficult.
You can read a comparison of Isolator++ and Google Mock here.
Use Dependency Injection
If you are writing new code, or able to change legacy code, dependency injection (DI) is your friend. While you may only associate DI with Java, it's available in C++, too. There's even a Boost library in the works.
But you don't need a framework, to use DI. If you don't want to use an experimental library or move to a new version of C++ yet, you can still use DI in your C++ code.
The most basic form of DI is constructor injection. Fowler describes it in the article I linked above using a framework for Java, but it's easy enough to roll your own if you don't want to add a new external dependency.
DI means separating the creation of an object from its use. This pattern makes it easy to replace the implementation of a service with a new one. Fowler even uses the term plugin to describes injected objects in his post.
But DI makes testing your code easier too. You can pass a fake to an object on initialization and then use it to observe the object under test. Dependency injection makes isolating objects for verification easy.
If you are working with complicated and tightly coupled legacy code, a mocking framework like Isolator++ can help you create tests. The framework is ideal for adding tests to legacy code. But writing your code with a pattern like DI will always make things easier and refactoring that legacy code might be a good idea too.
Write AAA Tests
Tests that are hard to decipher are almost as bad as no tests at all. (But only almost.)
Like clean code, there's such a thing as clean tests. Clean tests are easy to read and focus on a single piece of behavior.
One way to make sure your tests are easy to comprehend is implementing the three A's: Arrange, Act, and Assert. If you structure your tests with this in mind, they will be coherent and comprehensive.
Arrange gathers the test requirements and prepares them for the test. It's where you create the object to test and set any preconditions it needs. It's also where you create your mocks.
Act is the test operation itself. It's usually the short part of the test. Ideally, it's where you call a single function or method.
Assert verifies that the test succeeded. It's where you might see a list of assert statements that check the state of different variables.
The Mandate of Unit Testing With C++
There's no reason to write C++ code without unit tests. The patterns and practices associated with testing are all possible with the language because, as we all know, there is nothing you can do with another language that you can't with C++. Most other languages are implemented in C++ under the covers.
So, get started today! Isolator++ supports C++ on Windows and Linux and contains features that will help get you started with unit testing right away. Download a trial license today.
Published at DZone with permission of Eric Goebelbecker , DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.