Integrating GoogleTest with CMake
Learn to refactor manual C++ tests into a professional test suite using the GoogleTest framework, managed by vcpkg and integrated with CTest.
In the last lesson, we took our first steps into automated testing. We created a manual test driver, registered it with add_test()
, and ran it with CTest. While this worked, it also exposed the limitations of a manual approach.
This lesson introduces GoogleTest (often called gtest
), a feature-rich, industry-standard testing framework for C++. We'll refactor our clunky manual test into a clean and scalable test suite.
We'll manage gtest
as a dependency with vcpkg, integrate it into our build using CMake, and learn how to automatically discover and register our tests with CTest.
The Shortcomings of Manual Tests
For reference, here is the test driver we wrote in the last lesson.
tests/main.cpp
#include <greeter/Greeter.h>
#include <string>
#include <iostream>
int main() {
const std::string expected{
"Hello from the modular greeter library"
};
const std::string actual{get_greeting()};
if (actual != expected) {
std::cerr << "Test Failed:"
<< "\n Expected: " << expected
<< "\n Actual: " << actual;
return EXIT_FAILURE;
}
std::cout << "Test Passed!";
return EXIT_SUCCESS;
}
While it gets the job done for a single check, this approach has several major flaws:
- Excessive Boilerplate: Every single assertion requires a verbose
if/else
block, manual error message formatting, and areturn
statement. Adding ten more tests means copying this boilerplate ten more times. - Poor Failure Reporting: Our failure messages are entirely manual. If we forget to print the
expected
andactual
values, we'll just get a generic "Test Failed!" message with no context. - No Test Isolation: What if we wanted to add a second, unrelated test to this file? If the first test fails,
main()
returns immediately, and the second test is never run. There's no way to run multiple independent test cases and get a report on all of them.
A dedicated testing framework solves all of these problems.
Introducing Google Test
Google Test provides a structured environment for writing and running tests. It handles the boring parts for you so you can focus on the test logic itself. Its core value comes from two features:
- Test Macros: It provides a set of assertion macros (
EXPECT_EQ
,ASSERT_TRUE
,EXPECT_NE
, etc.) that automatically handle the comparison, generate detailed failure messages, and allow test execution to continue even after a failure. - Test Cases and
main()
: It provides aTEST()
macro to define independent, named test cases. It also provides its ownmain()
function, so you don't have to write one. It finds all yourTEST()
cases, runs them, and produces a summary report.
Conceptually, our manual test can be replaced with a single, expressive line of gtest
code:
EXPECT_EQ(
get_greeting(),
"Hello from the modular greeter library"
);
This is cleaner and far more readable. Google Test also includes a suite of more advanced features to solve more complex testing requirements. Let's integrate it into our project.
Setup with vcpkg and CMake Presets
First, we need to add gtest
as a dependency. We'll use the vcpkg version in this example, but it's also available through Conan, and the source code is on the official GitHub repository.
Step 1: Update vcpkg.json
Let's add "gtest"
to the dependencies
array in vcpkg.json
file at the project root.
vcpkg.json
{
"name": "greeter",
"dependencies": [
"gtest"
]
}
Step 2: Update CMake Presets
As always, to ensure find_package()
can locate the gtest
library installed by vcpkg, our CMake configuration needs to use the vcpkg toolchain file.
We'll handle this in the way we walked through in .
We'll define a hidden _base
preset that sets the toolchainFile
for all other presets that inherit from it. This ensures our entire build workflow is vcpkg-aware. We'll read the path to our vcpkg installation from the VCPKG_ROOT
environment variable:
CMakePresets.json
{
"version": 3,
"configurePresets": [{
"name": "_base",
"hidden": true,
"binaryDir": "${sourceDir}/build",
"toolchainFile":
"$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
}, {
"name": "default",
"displayName": "Default Config",
"inherits": "_base"
}],
"buildPresets": [{
"name": "default",
"configurePreset": "default"
}],
"testPresets": [{
"name": "default",
"configurePreset": "default",
"output": { "outputOnFailure": true }
}]
}
Remember to set this VCPKG_ROOT
environment variable if you haven't already, or update the toolchainFile
argument with the hardcoded location of your vcpkg installation.
Step 3: Update tests/CMakeLists.txt
Now, in tests/CMakeLists.txt
, we can use find_package()
to find gtest
. Because our preset has configured the toolchain, this command should find the version installed by vcpkg.
We'll also delete the add_test()
command, and implement gtest
's alternative approach soon:
tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
find_package(GTest REQUIRED)
add_executable(GreeterTests main.cpp)
target_link_libraries(GreeterTests PRIVATE GreeterLib)
add_test(
NAME Greeter.API.get_greeting
COMMAND GreeterTests
)
The find_package(GTest)
command does two important things:
- It confirms that the
gtest
library is available. - It makes the imported targets
GTest::gtest
andGTest::gtest_main
, and the commandgtest_discover_tests()
available. We'll use all of these soon.
Refactoring to a Google Test Case
With the setup complete, we can now refactor our test.
Step 1: Update the Test Code
First, let's delete the entire contents of tests/main.cpp
. We'll replace our manual main()
function with a gtest
TEST()
macro.
tests/main.cpp
#include <gtest/gtest.h>
#include <greeter/Greeter.h>
TEST(GreeterAPITests, GetGreetingReturnsCorrectString) {
const std::string expected{
"Hello from the modular greeter library"
};
EXPECT_EQ(get_greeting(), expected);
}
Let's break down the TEST()
macro:
- The first argument,
GreeterAPITests
, is the Test Suite name. This is a way to group related tests. - The second argument,
GetGreetingReturnsCorrectString
, is the individual Test Case name.
Test suites and tests have the same naming rules as C++ variables. Within those rules, we can call our test suite and tests whatever we want, but we should be descriptive.
Inside the curly braces, we have the test body. The EXPECT_EQ(val1, val2)
macro checks if val1
is equal to val2
. If it's not, it registers a failure and prints a detailed message, but allows the test program to continue running other tests.
Step 2: Link the gtest
Libraries
Next, we update tests/CMakeLists.txt
to link our GreeterTests
executable against the libraries provided by gtest
.
tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
find_package(GTest REQUIRED)
add_executable(GreeterTests main.cpp)
target_link_libraries(GreeterTests PRIVATE
GreeterLib
GTest::gtest # The core gtest library
GTest::gtest_main # Provides a main() function
)
GTest::gtest
: This is the coregtest
library containing all the assertion macros and test infrastructure.GTest::gtest_main
: This is a tiny library that contains just one thing: a pre-writtenmain()
function that discovers and runs all theTEST()
cases in your executable. By linking to it, we save ourselves from writing any boilerplate.
Integrating with CTest
The final step is to tell CTest about our new gtest
-based executable. We could use add_test()
as before, but the FindGTest
module we loaded through our find_package(GTest)
command provides a much better way: gtest_discover_tests()
.
You give it your test executable target, and at build time, it will run the executable with the --gtest_list_tests
flag to discover all the TEST()
cases inside it. It then automatically creates a separate CTest test for each one, using the full TestSuite.TestCase
name.
Let's replace our old add_test()
call with this new command:
tests/CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
find_package(GTest REQUIRED)
add_executable(GreeterTests main.cpp)
target_link_libraries(GreeterTests PRIVATE
GreeterLib GTest::gtest GTest::gtest_main
)
gtest_discover_tests(GreeterTests)
Now, let's run our test preset from the project root. Remember to clean your build directory first if you're making these changes to an existing project. Also ensure you still have the enable_testing()
command in your root CMakeLists.txt
file.
cmake --preset=default
cmake --build --preset=default
ctest --preset=default
The CTest output is now much more descriptive. It shows the full, namespaced gtest
name, which is helpful for identifying exactly which test passed or failed in a large suite.
Test project D:/Projects/cmake/build
Start 1: GreeterAPITests.GetGreetingReturnsCorrectString
1/1 Test #1: GreeterAPITests.GetGreetingReturnsCorrectString ... Passed 0.01 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.02 sec
Using Fixtures for Class-Based Testing
Free functions are easy to test, but most non-trivial C++ code is organized into classes. To set the stage for more complex tests, let's refactor our GreeterLib
from a free function into a simple class.
Files
This change presents a new challenge for our test. To test the greet()
method, we first need to create an instance of the Greeter
class.
tests/main.cpp
#include <gtest/gtest.h>
#include <greeter/Greeter.h>
TEST(GreeterClassTests, GreetReturnsCorrectString) {
Greeter my_greeter; // Create an object
EXPECT_EQ(
my_greeter.greet(),
"Hello from the Greeter class!"
);
}
This works, but if we have many tests for the Greeter
class, we'll be repeating the Greeter my_greeter;
line in every single test case. And, in real world examples, getting our object into a state where it is ready to be tested can involve significant amounts of code. This is where test fixtures come in.
A test fixture is a class that inherits from testing::Test
and is used to manage a shared test environment.
Let's create a fixture for our Greeter
tests:
tests/main.cpp
#include <gtest/gtest.h>
#include <greeter/Greeter.h>
// A test fixture for Greeter tests
class GreeterTest : public testing::Test {
protected:
Greeter my_greeter;
};
We can treat this class like any other - we can add variables and functions to it and inherit more classes from it as needed.
Most commonly, if we need to perform common initialization for a group of tests, we'd define a constructor or override
the SetUp()
method.
The TEST_F
Macro
By using the TEST_F
macro (the 'F' stands for Fixture), we tell gtest
that this test case belongs to the GreeterTest
fixture.
tests/main.cpp
#include <gtest/gtest.h>
#include <greeter/Greeter.h>
class GreeterTest : public testing::Test {
protected:
// This object will be created fresh for each TEST_F
Greeter my_greeter;
};
// Use the TEST_F macro to use the fixture
TEST_F(GreeterTest, GreetReturnsCorrectString) {
EXPECT_EQ(
my_greeter.greet(),
"Hello from the Greeter class!"
);
}
Note that, unlike TEST
, the first argument to TEST_F
is no longer an arbitrary name - it is now the name of the class
that we created to set up our testing requirements.
Before running each test, gtest
will create an instance of this GreeterTest
class. Our test can then access any of its protected
or public
members. In our example, the only member is our my_greeter
object, but these testing::TEST
-derived classes can get as complicated as we need them to be.
This makes the fixture-based pattern useful for more complex tests that require setting up a common state (like opening a file or connecting to a test database) before each test runs.
Summary
In this lesson, we transformed our basic, manual test into a test suite using GoogleTest. This unlocks a huge leap forward in the quality and maintainability of our project's tests.
- Using Test Frameworks: We saw how a dedicated testing framework like
gtest
can eliminate boilerplate and provide utilities that solve common testing requirements. - vcpkg Integration: We added
gtest
as a dependency in ourvcpkg.json
manifest. - Refactoring to
gtest
: We replaced our manualmain()
with aTEST()
macro andEXPECT_EQ
, and linked against theGTest::gtest
andGTest::gtest_main
imported targets. - Automated Discovery: We replaced
add_test()
withgtest_discover_tests()
, which automatically finds and registers allgtest
cases with CTest. - Test Fixtures: We introduced test fixtures (
class MyTest : public testing::Test
) and theTEST_F()
macro as the standard way to manage shared setup and teardown logic for class-based tests.
Assertions and Parameterized Tests
Learn to use a wide range of assertions, write data-driven parameterized tests, and isolate dependencies with Google Mock.