Skip to Content
Course content

134: Practice Exercise: Setting Up a CMake Project with Tests

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How should I actually organize my folders so CMake doesn't become a mess?

I've seen too many beginners dump everything into one directory. It works for a tiny project, but the moment you add tests, it becomes a nightmare of filenames. The standard professional approach is to separate your "production" code from your "test" code.

For this exercise, imagine we're building a PrimeGenerator utility. I'd set it up like this:

  • /project_root
    ├── CMakeLists.txt (The main entry point)
    ├── src/ (Your actual logic)
    │ ├── CMakeLists.txt
    │ ├── PrimeGenerator.cpp
    │ └── PrimeGenerator.hpp
    └── tests/ (Your test suite)
    ├── CMakeLists.txt
    └── test_main.cpp

By splitting the CMakeLists.txt files, you keep the logic modular. The root file handles the project-wide settings, while the subdirectories handle their own targets.

How do I avoid compiling my source files twice for the app and the tests?

This is where a lot of people trip up. They try to add PrimeGenerator.cpp to both the main executable and the test executable. Not only is that slow, but it can lead to weird linking errors.

The trick is to compile your logic as a library. You create a library target that both your main app and your tests link against. In your src/CMakeLists.txt, it looks like this:

# Create a library from the source files
add_library(PrimeLib PrimeGenerator.cpp)

# Make sure whoever links to this can find the headers in this directory
target_include_directories(PrimeLib PUBLIC .)

Now, in your tests/CMakeLists.txt, you don't list the source files again. You just link the library:

add_executable(unit_tests test_main.cpp)
target_link_libraries(unit_tests PRIVATE PrimeLib)

What's the cleanest way to bring in a testing framework like GoogleTest?

Back in the day, you had to manually install libraries on your system, which made collaborating with other developers a pain. Now, I always use FetchContent. It tells CMake to go grab the framework from GitHub during the configuration step, so the project is "self-contained."

In your root CMakeLists.txt, add this:

include(FetchContent)

FetchContent_Declare(
  googletest
  URL https://github.com/google/googletest/archive/refs/heads/main.zip
)
# For Windows, this prevents GTest from overriding the runtime library settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

Once that's done, you just link gtest_main to your test executable, and you don't even have to write your own main() function for the tests—GoogleTest provides one for you.

How do I actually run the tests once everything is configured?

You don't just run the test executable manually; you use ctest. It's a command-line tool that comes with CMake designed specifically to run your test suite and report failures in a clean format.

First, you must call enable_testing() in your root CMakeLists.txt. Then, in your tests/CMakeLists.txt, you register the test:

add_test(NAME PrimeTests COMMAND unit_tests)

After you build the project, go to your build directory and simply type:

ctest --output-on-failure

I always add the --output-on-failure flag. Otherwise, CTest just tells you a test failed, but it won't show you the actual error message from the framework, which is incredibly frustrating when you're trying to debug a failing assertion.




📋 Practical Task

Project Setup: Validated Matrix Multiplication Library

Your task is to set up a complete CMake project structure for a Matrix Math library. You need to ensure the project is modular and the tests are decoupled from the main application.

  • Step 1: Create the directory structure with src/ and tests/ folders.
  • Step 2: In src/, implement a simple Matrix class that can perform a 2x2 matrix multiplication.
  • Step 3: Configure the src/CMakeLists.txt to build this as a library named MatrixLib.
  • Step 4: In the root CMakeLists.txt, use FetchContent to integrate Catch2 or GoogleTest.
  • Step 5: In tests/, create a test suite that verifies:
    • Multiplying a matrix by the Identity matrix returns the original matrix.
    • Multiplying two zero matrices returns a zero matrix.
  • Step 6: Use add_test and enable_testing() so that you can execute the entire suite using the ctest command.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.