Unit testing your C code
16 Oct 2024Introduction
Writing reliable and maintainable code is a fundamental part of software development, and unit testing is one of the most effective ways to ensure your code works as expected. Unit tests help catch bugs early, ensure that changes to the codebase don’t introduce new issues, and serve as a form of documentation for your code’s expected behavior.
In this article, we’ll explore how to set up and use Google Test (also known as googletest), a popular C++ testing framework, to test your C and C++ code. We’ll walk through the installation process, demonstrate basic assertions, and then dive into testing a more complex feature—the variant data type.
Installation and Setup
Google Test makes it easy to write and run unit tests. It integrates well with build systems like CMake, making it straightforward to include in your project. Let’s go step-by-step through the process of installing and setting up Google Test in a CMake-based project.
Step 1: Add Google Test to Your Project
First, we need to download and include Google Test in the project. One of the easiest ways to do this is by adding Google Test as a subdirectory in your project’s source code. You can download the source directly from the Google Test GitHub repository.
Once you have the source, place it in a lib/
directory within your project.
Your directory structure should look something like this:
Step 2: Modify the CMake File
Now that you have Google Test in your project, let’s modify your CMakeLists.txt
file to integrate it. Below is an example of a CMake configuration that sets up
Google Test and links it to your test suite:
This CMake setup includes Google Test in your project by adding it as a
subdirectory, and it links your test suite to the gtest
and gtest_main
libraries. Now you’re ready to write and run unit tests!
Step 3: Build and Run the Tests
To compile the tests, simply run the following commands from the root of your project directory:
Once the build is complete, you can run your tests with:
This command will execute all the tests defined in your test files. Now that the environment is set up, let’s move on to writing some unit tests using Google Test.
Basic Assertions with Google Test
Before diving into testing our variant data type, let’s explore some of the basic assertions provided by Google Test. Assertions are used to check that a particular condition holds true during test execution. If the condition is false, the test fails.
Common Assertions
Here are some of the most commonly used assertions:
EXPECT_EQ(val1, val2)
: Checks thatval1
is equal toval2
.EXPECT_NE(val1, val2)
: Checks thatval1
is not equal toval2
.EXPECT_TRUE(condition)
: Checks that the condition istrue
.EXPECT_FALSE(condition)
: Checks that the condition isfalse
.ASSERT_EQ(val1, val2)
: LikeEXPECT_EQ
, but if the assertion fails, it aborts the current function.
Let’s look at a simple example that tests basic operations:
When you run this test, Google Test will evaluate each assertion and output the result. If any assertion fails, it will print a detailed message showing the expected and actual values.
Now that you’ve seen how to use basic assertions, let’s move on to testing a more complex feature: the variant data type.
Testing the Variant Data Type
In a previous post we explored creating our own variant data type. This piece of library code should provide us with some good examples on how to apply unit tests.
With the variant being able to hold multiple types (integers, floats, strings, etc.), we need to test that each type is correctly handled by the variant and behaves as expected.
Here’s an example test that checks if the ced_var_new_int8
function correctly
creates an 8-bit integer variant:
This test ensures that:
- The variant type is correctly set to
ced_var_type_int8
. - The integer value stored in the variant is
1
.
You can follow this pattern to test other data types supported by the variant, ensuring that each type is correctly initialized and behaves as expected.
In the next section, we’ll walk through more examples of testing different variant types and introduce more complex tests for arrays and type conversions.
More Tests!
Now that we’ve covered the basics of using Google Test, let’s look at some examples of how to apply these concepts to test our variant data type. We won’t go through every single test, but we’ll highlight a few that demonstrate different key behaviors—constructing basic types, handling arrays, and type conversion.
Constructing Basic Types
One of the simplest tests you can write is to verify that a variant can be properly constructed with a specific data
type. This ensures that the ced_var_new_*
functions correctly initialize the variant.
For example, here’s a test that checks if we can create an 8-bit integer variant:
This test checks the following:
- The variant’s type is correctly set to
ced_var_type_int8
. - The data inside the variant is the expected integer value.
- The variant is freed properly at the end to avoid memory leaks.
Handling Arrays of Variants
Another important feature of the variant data type is its ability to hold arrays of other variants. Testing this involves creating an array, verifying its size, and ensuring each element in the array holds the correct value.
Here’s an example that constructs an array of variants and tests its contents:
In this test, we:
- Create an array of variants, each holding different types (integers and a string).
- Verify that the variant we created is indeed an array.
- Check that the size of the array is correct.
- Clean up the memory for each individual variant and the array as a whole.
Type Conversion and Safety
Variants allow us to convert between different types, but not all conversions are valid. We should ensure that the type conversion logic works correctly and fails gracefully when an invalid conversion is attempted.
Let’s look at a test that checks a valid conversion, and another that ensures a failed conversion returns NULL
:
Successful Type Conversion
This test checks that:
- The original 8-bit integer is correctly converted into a 16-bit integer.
- The value remains unchanged after conversion.
Failed Type Conversion
In this test:
- We attempt to convert a 64-bit integer into an 8-bit integer, which is not possible.
- The conversion returns
NULL
, indicating the failure, and we verify this withEXPECT_EQ
.
These are just a few examples of the types of unit tests you can write for your variant data type. By covering basic type construction, handling arrays, and ensuring type conversion behaves as expected, we’ve demonstrated how to use Google Test to validate the functionality of complex C code.
Conclusion
Unit testing is a critical part of ensuring the reliability and correctness of your code. By integrating Google Test into your C/C++ projects, you can create a robust testing suite that not only catches bugs early but also provides confidence in the stability of your codebase.
With the ability to handle various types, arrays, and even type conversions, our variant data type is a powerful tool, and Google Test helps ensure it works exactly as intended. Whether you’re dealing with basic types or more complex features, writing clear, concise unit tests like the ones shown here will go a long way in maintaining high-quality code.