C++ Projects, CMake, and Testing
Organize headers and source files, define build targets with CMake, enable compiler diagnostics, and test domain rules independently of the console.
Before this lesson
Separate declarations definitions and application wiring
Create CMake library executable and test targets
Test boundary behavior without console input
The short answer
Build around targets rather than global flags: a library owns domain code, an executable owns console wiring, and tests link the library. CMake describes those relationships while the compiler and test runner perform the work.
Files help people and targets define builds
Source files organize definitions; headers expose interfaces; a build target groups sources, compiler requirements, include paths, and dependencies into one library or executable. Splitting files does not create architectural value by itself—the target boundary states reusable ownership.
A small project can use a domain library, console executable, and test executable. This lets tests call rules without linking terminal behavior and lets another interface reuse the same library later.
task-tracker/
CMakeLists.txt
include/task_tracker/task.hpp
src/task.cpp
app/main.cpp
tests/task_tests.cppHeaders declare stable interfaces
Put declarations needed by callers in headers and implementation details in source files. Use include guards or #pragma once, include what the header itself needs, and avoid broad namespace imports in headers because they affect every includer.
Changing a public header can rebuild many translation units. Keep interfaces focused and prefer forward declarations only when they are valid and genuinely simplify dependencies.
CMake connects targets and requirements
Modern CMake attaches requirements to targets. target_compile_features requests a language level, target_include_directories publishes include paths, and target_link_libraries states dependencies. Configure into a separate build directory so generated files do not mix with source.
cmake_minimum_required(VERSION 3.20)
project(TaskTracker LANGUAGES CXX)
add_library(task_tracker src/task.cpp)
target_include_directories(task_tracker PUBLIC include)
target_compile_features(task_tracker PUBLIC cxx_std_20)
add_executable(task_tracker_app app/main.cpp)
target_link_libraries(task_tracker_app PRIVATE task_tracker)
enable_testing()
add_executable(task_tracker_tests tests/task_tests.cpp)
target_link_libraries(task_tracker_tests PRIVATE task_tracker)
add_test(NAME task_tracker_tests COMMAND task_tracker_tests)Tests protect behavior at boundaries
A focused test arranges input, calls one public behavior, and checks the observable result. Boundary cases concentrate defects: just below, exactly at, and just above a threshold. See a new test fail for the expected reason before trusting its success.
Assertions from cassert are enough to learn the structure but can be disabled in release builds and provide limited reporting. A test framework adds discovery, fixtures, parameterized cases, and diagnostics. Keep tests independent of order, working directory, network, and private implementation details.
| Test level | Useful evidence | Avoid |
|---|---|---|
| Unit | One rule over controlled values | Private-method coupling |
| Integration | Real collaboration such as file storage | Machine-specific persistent data |
| End to end | User-visible command flow | Using it for every edge case |
| Static analysis | Suspicious source patterns | Treating it as runtime proof |
Quick knowledge check
Answer before you reveal.
01Does a header normally contain every non-template definition?
No. It commonly declares the public interface while source files hold definitions; templates and inline definitions are important exceptions.
02Why test the domain library instead of main?
Typed functions can be called with controlled values without simulating terminal input and parsing output.
Exercise
Practice challenge
Create a CMake project with a shipping library, a console executable, and a test executable. Test subtotal values below at and above the free-shipping boundary.
Requirements
- The shipping library contains no console calls
- CMake builds all targets from a clean build directory
- CTest reports all boundary tests passing
Optional extension: Run the build with GCC and Clang warnings in continuous integration.
Open in C++ compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Is CMake a compiler?
No. It generates or drives a native build system using configured compilers, targets, sources, and requirements.
Which C++ test framework should I use?
Catch2, GoogleTest, doctest, and others are established. Follow project conventions and focus on useful cases and stable behavior contracts.
Should warnings be global CMake flags?
Prefer target-specific options so dependencies and unrelated targets do not inherit requirements accidentally.