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.

Greg Filak
Published

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 a return 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 and actual 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:

  1. 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.
  2. Test Cases and main(): It provides a TEST() macro to define independent, named test cases. It also provides its own main() function, so you don't have to write one. It finds all your TEST() 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:

  1. It confirms that the gtest library is available.
  2. It makes the imported targets GTest::gtest and GTest::gtest_main, and the command gtest_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.

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 core gtest library containing all the assertion macros and test infrastructure.
  • GTest::gtest_main: This is a tiny library that contains just one thing: a pre-written main() function that discovers and runs all the TEST() 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

greeter
app
Select a file to view its content

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 our vcpkg.json manifest.
  • Refactoring to gtest: We replaced our manual main() with a TEST() macro and EXPECT_EQ, and linked against the GTest::gtest and GTest::gtest_main imported targets.
  • Automated Discovery: We replaced add_test() with gtest_discover_tests(), which automatically finds and registers all gtest cases with CTest.
  • Test Fixtures: We introduced test fixtures (class MyTest : public testing::Test) and the TEST_F() macro as the standard way to manage shared setup and teardown logic for class-based tests.
Next Lesson
Lesson 49 of 55

Assertions and Parameterized Tests

Learn to use a wide range of assertions, write data-driven parameterized tests, and isolate dependencies with Google Mock.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate