Featured Webinar: AI-Enhanced API Testing: A No-Code Approach to Testing | Watch Now
Jump to Section
When to Mock Unit Testing C/C++ Code
Unit testing is the process of separating units and executing independent tests on each one. This article includes comprehensive guidance on when to mock unit tests and some helpful pointers for C and C++ unit testing.
Jump to Section
Jump to Section
Unit testing is all about testing isolated units or functions and operations. In this post, we look at various opportunities for mocking, including addressing some common questions to effectively guide you through C and C++ unit testing.
Starting with an example, I often get the question, “How much unit test isolation do we need?” This is a recurring and important question commonly debated when developing unit tests for C and C++.
I’m not talking here about the isolation from the fellow developer sitting next to us in the open space and drumming the rhythm of the music from his headphones, which, by the way, is also very important when we want to create good-quality code. I’m talking about the isolation of the tested code from its surrounding environment—its so-called collaborators.
Before I continue, let me just clear one thing up. When discussing stubbing and mocking for C and C++ languages, usually there’s a line drawn between C and C++ because of the differences in the language layer reflected in the complexity, capabilities, and expectations regarding typical mocking frameworks.
With Parasoft C/C++test, the situation is slightly different because most of the framework capabilities are available for both languages. So, when discussing this subject, I’ll be giving either a C or C++ unit test example, and unless I specifically mark something as supported only for C or C++, you should always assume that specific functionality is provided for both languages.
What Is Mocking & How Does It Work in Testing?
Definitions are known to be inconsistent and have caused some confusion. Therefore, this merits a quick explanation of what is a stub. Then explaining what is a mock is easier.
The purpose of both is to replace a piece of code or dependency outside of the unit. Eliminating all the dependencies of a unit or function allows your tests to focus on testing the quality (safety, security, and reliability) of the unit.
Stubbing replaces dependencies but it’s a simple implementation that returns canned values. Mocks focus on behavior, so they’re an implementation that’s controlled by the unit test. They can be implemented with return values, check values of arguments, and help verify common safety and security requirements functionality. However, in all honesty, when I’m creating my unit test cases, I don’t think about whether I’m stubbing or mocking. I’m just focused on testing the functionality to determine if it satisfies my requirements and whether the implementation is robust.
To Isolate or Not to Isolate?
For some folks, common sense dictates that we should not isolate unless we have a good reason for it. Testing by including other collaborators or functionality only increases our penetration of the code base. And why should we pass up the opportunity to obtain some extra code coverage and the possibility of finding bugs outside the unit? Well, there are good reasons for it.
An orthodox unit tester will argue that unit testing is about testing the isolated units and it should stay that way. If each individual unit is sound, it only strengthens the whole. Besides, testing with real collaborators or including other functions in the test is referred to as integration testing. If we include all of the code, then test cases are referred to as system testing.
There are different levels of abstraction. Developers and testers should perform testing at these different levels. The lowest level is called unit testing and you must perform isolation of the unit. Let’s go through some reasons for mocking and best practices in isolating units.
Reasons to Mock Units in Your Testing Process
1. Collaborator Not Yet Implemented or Still Under Development
This is a simple one. We do not have a choice, and we need a mock implementation. The diagram below illustrates this typical unit test environment (SUT – system under test, DOC – dependent component/collaborator):
2. Hardware Independence
For developers writing desktop applications, this class of problems may seem distant. But for embedded developers, hardware independence of unit tests is an important aspect that allows for high-level test automation and execution without a need for hardware.
A good example here would be a unit under test interacting with GPS hardware, expecting a certain sequence of localization coordinates to be provided to compute velocity. Although it’s a good idea that we exercise more, I can’t imagine testers running around with a device in order to simulate movement, just to generate the required test inputs, any time a unit testing session is required. To that end, this example illustrates just how late in the development lifecycle GPS testing of a device would be if hardware independence wasn’t possible during development.
3. Fault Injection
Injecting errors on purpose is a common scenario in testing. This might be used, for example, to test whether memory allocation has failed or to see if a hardware component has failed. Some developers try to stimulate the real collaborator in the test initialization phase so that it responds with an error when called from the tested code. For me, this is not practical and is usually too much hassle. A test-specific, fake implementation that simulates a fault is usually a much better choice.
Best Practices for Implementing Mocking in Your Testing Process
Besides these obvious cases where a mocked collaborator is always desired, there are some other, more subtle situations or best practices where mocking or adding fake collaborators is a good choice. Also, if your testing process suffers from any of the problems listed below, it’s an indication that better isolation of the tested code is required.
1. When Unit Tests Are Not Repeatable
Transience may be an issue that makes it difficult to implement stable tests. There are cases where units depend on an external signal to indicate behavior. A classic example is a unit that relies on the system clock for its behavior. For example, if a unit reacts differently at certain points in time, then automation is difficult to achieve. A best practice is to mock the call to the system clock and obtain full control over the time input values.
2. When Test Environments Are Difficult to Initialize
Initializing the test environment can be very complex. Simulating the real collaborators so that they provide reliable inputs to the tested code may be a daunting task, if not impossible.
Components are often interrelated. When trying to initialize one specific module, we may end up initializing half of the system. Replacing the real collaborators with a mock or fake implementation reduces the complexity of test environment initialization.
3. When Test Status Is Difficult to Determine
In many cases, determining the test verdict requires checking the state of the collaborator after the test is executed. With real collaborators, it’s often impossible because there is no suitable access method in the real collaborator interface to query the state after the test.
Replacing a real collaborator with a mock usually fixes the problem. We can extend the fake implementation with all kinds of access methods to determine the test result.
4. When Tests Are Slow
There are cases when a response from the real collaborator can take a considerable amount of time. It’s not always clear when the delay becomes unacceptable and when isolation is required. Is a two-minute delay in a test run acceptable or not?
It’s often desirable to be able to run test suites as quickly as possible, perhaps after each code change. Large delays due to interactions with real collaborators can make test suites too slow to be practical. Mocks of these real collaborators can be faster by several orders of magnitude and bring the test execution time to an acceptable level.
Practical Example of When to Mock Unit Testing C/C++ Code
Mocking interfaces can make the job of testing much easier. Instead of your unit calling others, it can call a mock interface. Your testing code can interpose itself on all sides of the unit you wish to test, and then, by examining all outputs and handling all inputs.
Let’s say that in the following code example, we want to test the bar unit/function and have it call a fake/mock foo function. To do so, we need a way to replace and control the call to foo(). There are several mocking approaches to do this and Parasoft can automate much of this for you. In this example and for simplicity purposes, a macro (#define FOO) is used to control the collaborator foo().
#ifdef TEST #define FOO mock_foo #else #define FOO foo #endif int mock_foo(int x) { return x; } int bar(int x) { int result = 0; for (int i = 0; i < 10; i++) { result += FOO(i + x); } return result; }
Helpful Questions to Determine Whether or Not to Mock
When writing a new C or C++ unit test and deciding about using original collaborators or mocked implementations, consider the following four questions.
- Is the real collaborator a source of risk for the stability of my tests?
- Is it difficult to initialize the real collaborator?
- Is it possible to verify the state of the collaborator after the test, to decide the test status?
- How long will it take for the collaborator to respond?
If we know the collaborator well enough to answer all these questions, then it’s an easy decision one way or the other. If not, then I would suggest starting with the real collaborator and trying to answer the four questions as you go. In practice, this is the approach that most test-driven development (TDD) practitioners apply in their daily work. It means you need to take due care of your test cases and review them closely until they become stable.
Most often, unit test isolation is complicated by the dependencies of the unit under test. There are clearly desirable cases where mocking a dependent component is needed, but also more subtle situations as well. In some cases, it isn’t clear cut and depends on the risk and uncertainty that a dependency has in the test environment.