Docs
README
Build Tools and Testing in C++
Module Overview
This module covers essential development tools and practices for professional C++ development: build systems, debugging, and testing.
Table of Contents
- •Why Build Tools Matter
- •CMake Essentials
- •GDB Debugging
- •Unit Testing with Google Test
- •Integrating Everything
- •Best Practices
Topics
1. CMake Basics
Learn the industry-standard build system for C++ projects.
- •CMakeLists.txt structure
- •Targets and dependencies
- •Finding packages
- •Modern CMake practices
2. Debugging with GDB
Master debugging techniques for C++ programs.
- •GDB commands
- •Breakpoints and watchpoints
- •Stack inspection
- •Memory debugging
3. Unit Testing
Write reliable tests for your C++ code.
- •Google Test framework
- •Writing test cases
- •Test fixtures
- •Mocking and test doubles
Why These Skills Matter
┌─────────────────────────────────────────────────────────────────────────┐
│ PROFESSIONAL C++ WORKFLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ WRITE │────►│ BUILD │────►│ TEST │────►│ DEBUG │ │
│ │ CODE │ │ (CMake) │ │ (GTest) │ │ (GDB) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ ITERATE │
│ │
└─────────────────────────────────────────────────────────────────────────┘
The Problem with Manual Compilation
# Simple project - manageable
g++ main.cpp -o app
# Real project - nightmare!
g++ -std=c++17 -Wall -Wextra -I./include -I./libs/json/include \
-L./libs/boost/lib -lboost_filesystem -lboost_system \
src/main.cpp src/utils.cpp src/network.cpp src/database.cpp \
-o myapp -pthread -lssl -lcrypto
Problems solved by build tools:
- •Hard to remember all flags
- •Different commands for Debug vs Release
- •Cross-platform differences (Windows vs Linux)
- •Managing dependencies is manual
- •No incremental compilation
CMake Essentials
What is CMake?
CMake is a meta build system - it generates native build files:
- •
Makefileon Linux/Mac - •Visual Studio projects on Windows
- •Xcode projects on Mac
Minimal CMakeLists.txt
# Minimum CMake version required
cmake_minimum_required(VERSION 3.16)
# Project name and language
project(MyApp VERSION 1.0 LANGUAGES CXX)
# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Create executable from source files
add_executable(myapp main.cpp)
Building with CMake
# Step 1: Create build directory (out-of-source build)
mkdir build && cd build
# Step 2: Generate build files
cmake ..
# Step 3: Build the project
cmake --build .
# Or use make directly
make
# Run the executable
./myapp
Adding Multiple Source Files
# Method 1: List explicitly (recommended)
add_executable(myapp
main.cpp
src/utils.cpp
src/network.cpp
)
# Method 2: Glob (not recommended - misses new files)
file(GLOB SOURCES "src/*.cpp")
add_executable(myapp ${SOURCES})
Creating Libraries
# Static library (.a on Linux, .lib on Windows)
add_library(mylib STATIC
lib/math.cpp
lib/string_utils.cpp
)
# Shared library (.so on Linux, .dll on Windows)
add_library(mylib SHARED
lib/math.cpp
)
# Link library to executable
target_link_libraries(myapp PRIVATE mylib)
Include Directories
# For a target (modern, preferred)
target_include_directories(myapp PRIVATE
${CMAKE_SOURCE_DIR}/include
)
# PUBLIC - propagates to dependents
# PRIVATE - only for this target
# INTERFACE - only for dependents, not this target
Compiler Flags
# Add warning flags
target_compile_options(myapp PRIVATE
-Wall -Wextra -Wpedantic
)
# Debug vs Release builds
# Debug: -g -O0
# Release: -O3 -DNDEBUG
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DCMAKE_BUILD_TYPE=Release ..
Finding External Libraries
# Find a package installed on system
find_package(Threads REQUIRED)
find_package(OpenSSL REQUIRED)
target_link_libraries(myapp PRIVATE
Threads::Threads
OpenSSL::SSL
)
Complete Project Structure
MyProject/
├── CMakeLists.txt # Root CMake file
├── src/
│ ├── CMakeLists.txt # Source CMake
│ └── main.cpp
├── include/
│ └── myapp/
│ └── utils.h
├── libs/
│ ├── CMakeLists.txt
│ └── mylib/
│ ├── mylib.cpp
│ └── mylib.h
├── tests/
│ ├── CMakeLists.txt
│ └── test_main.cpp
└── build/ # Build output (gitignored)
GDB Debugging
What is GDB?
GDB (GNU Debugger) is a powerful tool for:
- •Finding where your program crashes
- •Stepping through code line by line
- •Inspecting variable values
- •Setting breakpoints
Compiling for Debug
# Always use -g flag for debugging symbols
g++ -g -O0 main.cpp -o app
# With CMake
cmake -DCMAKE_BUILD_TYPE=Debug ..
Essential GDB Commands
| Command | Short | Description |
|---|---|---|
run | r | Start program |
quit | q | Exit GDB |
break main | b main | Set breakpoint at main |
break file.cpp:42 | b file.cpp:42 | Breakpoint at line 42 |
continue | c | Continue to next breakpoint |
next | n | Execute next line (step over) |
step | s | Step into function |
finish | fin | Run until current function returns |
print x | p x | Print variable x |
backtrace | bt | Show call stack |
list | l | Show source code |
info locals | Show all local variables | |
info breakpoints | i b | List breakpoints |
delete 1 | d 1 | Delete breakpoint 1 |
GDB Session Example
$ gdb ./myapp
(gdb) break main
Breakpoint 1 at 0x1234: file main.cpp, line 10.
(gdb) run
Starting program: ./myapp
Breakpoint 1, main () at main.cpp:10
10 int x = 5;
(gdb) next
11 int y = calculate(x);
(gdb) step
calculate (n=5) at math.cpp:3
3 return n * 2;
(gdb) print n
$1 = 5
(gdb) backtrace
#0 calculate (n=5) at math.cpp:3
#1 main () at main.cpp:11
(gdb) continue
[Inferior 1 exited normally]
(gdb) quit
Debugging Crashes
# When program crashes, GDB shows the crash location
(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x00005555555551a9 in processData (ptr=0x0) at main.cpp:15
15 *ptr = 42; # <-- The crash line!
(gdb) backtrace
#0 processData (ptr=0x0) at main.cpp:15
#1 main () at main.cpp:25
(gdb) print ptr
$1 = (int *) 0x0 # Null pointer!
Conditional Breakpoints
# Break only when condition is true
(gdb) break loop.cpp:20 if i == 100
# Break only on 5th hit
(gdb) break func.cpp:30
(gdb) ignore 1 4 # Skip first 4 hits
Watchpoints
# Break when variable changes
(gdb) watch myVariable
# Break when expression is true
(gdb) watch x > 100
TUI Mode (Visual Debugging)
# Start with TUI
gdb -tui ./myapp
# Toggle TUI inside GDB
(gdb) tui enable
(gdb) layout src # Show source
(gdb) layout asm # Show assembly
(gdb) layout split # Show both
Unit Testing with Google Test
What is Unit Testing?
Unit testing verifies individual "units" (functions, classes) work correctly:
- •Automated - run tests with one command
- •Repeatable - same results every time
- •Fast - catch bugs early
- •Documentation - tests show how code should work
Installing Google Test
# Ubuntu/Debian
sudo apt install libgtest-dev
# Build from source
cd /usr/src/gtest
sudo cmake .
sudo make
sudo cp lib/*.a /usr/lib/
# Or via vcpkg
vcpkg install gtest
CMake Integration
# Find or fetch Google Test
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
# Enable testing
enable_testing()
# Create test executable
add_executable(tests
tests/test_main.cpp
tests/test_math.cpp
)
target_link_libraries(tests PRIVATE
gtest
gtest_main
mylib # Your library being tested
)
# Add test to CTest
add_test(NAME unit_tests COMMAND tests)
Your First Test
#include <gtest/gtest.h>
// Function to test
int add(int a, int b) {
return a + b;
}
// Test case
TEST(MathTest, AdditionWorks) {
EXPECT_EQ(add(2, 3), 5);
EXPECT_EQ(add(-1, 1), 0);
EXPECT_EQ(add(0, 0), 0);
}
// Main function (optional with gtest_main)
int main(int argc, char** argv) {
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Assertions
| Assertion | Fatal Version | Description |
|---|---|---|
EXPECT_EQ(a, b) | ASSERT_EQ(a, b) | a == b |
EXPECT_NE(a, b) | ASSERT_NE(a, b) | a != b |
EXPECT_LT(a, b) | ASSERT_LT(a, b) | a < b |
EXPECT_LE(a, b) | ASSERT_LE(a, b) | a <= b |
EXPECT_GT(a, b) | ASSERT_GT(a, b) | a > b |
EXPECT_GE(a, b) | ASSERT_GE(a, b) | a >= b |
EXPECT_TRUE(x) | ASSERT_TRUE(x) | x is true |
EXPECT_FALSE(x) | ASSERT_FALSE(x) | x is false |
EXPECT_NEAR(a, b, tol) | ASSERT_NEAR(a, b, tol) | |a - b| < tol |
EXPECT vs ASSERT:
- •
EXPECT_*- continues after failure (preferred) - •
ASSERT_*- stops test immediately on failure
Test Fixtures
Use fixtures when multiple tests share setup/teardown:
class DatabaseTest : public ::testing::Test {
protected:
Database* db;
// Runs before each test
void SetUp() override {
db = new Database("test.db");
db->connect();
}
// Runs after each test
void TearDown() override {
db->disconnect();
delete db;
}
};
TEST_F(DatabaseTest, CanInsertRecord) {
EXPECT_TRUE(db->insert("key", "value"));
}
TEST_F(DatabaseTest, CanRetrieveRecord) {
db->insert("key", "value");
EXPECT_EQ(db->get("key"), "value");
}
Parameterized Tests
Test with multiple inputs:
class AddTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};
TEST_P(AddTest, AddsCorrectly) {
auto [a, b, expected] = GetParam();
EXPECT_EQ(add(a, b), expected);
}
INSTANTIATE_TEST_SUITE_P(
AdditionCases,
AddTest,
::testing::Values(
std::make_tuple(1, 2, 3),
std::make_tuple(-1, -1, -2),
std::make_tuple(0, 0, 0),
std::make_tuple(100, 200, 300)
)
);
Testing Exceptions
void mayThrow(int x) {
if (x < 0) throw std::invalid_argument("negative");
}
TEST(ExceptionTest, ThrowsOnNegative) {
EXPECT_THROW(mayThrow(-1), std::invalid_argument);
EXPECT_NO_THROW(mayThrow(1));
EXPECT_ANY_THROW(mayThrow(-5));
}
Running Tests
# Run all tests
./tests
# Run specific test
./tests --gtest_filter=MathTest.AdditionWorks
# Run tests matching pattern
./tests --gtest_filter=Math*
# List all tests
./tests --gtest_list_tests
# Run with verbose output
./tests --gtest_output=xml:results.xml
Integrating Everything
Complete Development Workflow
# 1. Write code
vim src/feature.cpp
# 2. Build
cd build && cmake --build .
# 3. Run tests
ctest --output-on-failure
# 4. Debug if tests fail
gdb ./tests
(gdb) break test_feature.cpp:25
(gdb) run --gtest_filter=FeatureTest.Fails
# 5. Fix and repeat
Complete Project CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(CompleteProject VERSION 1.0 LANGUAGES CXX)
# Global settings
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Options
option(BUILD_TESTS "Build unit tests" ON)
option(BUILD_DOCS "Build documentation" OFF)
# Compiler warnings
add_compile_options(-Wall -Wextra -Wpedantic)
# Debug symbols for debug builds
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
# Main library
add_library(mylib
src/core.cpp
src/utils.cpp
)
target_include_directories(mylib PUBLIC include)
# Main executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)
# Tests
if(BUILD_TESTS)
enable_testing()
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
add_executable(tests
tests/test_core.cpp
tests/test_utils.cpp
)
target_link_libraries(tests PRIVATE mylib gtest gtest_main)
add_test(NAME unit_tests COMMAND tests)
endif()
Best Practices
CMake Best Practices
✅ Do:
# Use modern target-based commands
target_include_directories(myapp PRIVATE include)
target_link_libraries(myapp PRIVATE mylib)
target_compile_options(myapp PRIVATE -Wall)
# Out-of-source builds
mkdir build && cd build && cmake ..
# Specify minimum CMake version
cmake_minimum_required(VERSION 3.16)
❌ Don't:
# Avoid old-style global commands
include_directories(include) # Use target_include_directories
link_libraries(mylib) # Use target_link_libraries
add_definitions(-DFOO) # Use target_compile_definitions
# Avoid GLOB for sources
file(GLOB SOURCES "src/*.cpp") # CMake won't detect new files
GDB Best Practices
✅ Do:
- •Always compile with
-g -O0for debugging - •Use breakpoints instead of print statements
- •Learn TUI mode for visual debugging
- •Use
.gdbinitfor custom settings
❌ Don't:
- •Debug optimized (
-O2,-O3) code - •Ignore warnings during compilation
- •Forget to check return values
Testing Best Practices
✅ Do:
// One assertion per test (when practical)
TEST(CalcTest, AddPositive) {
EXPECT_EQ(add(2, 3), 5);
}
// Test edge cases
TEST(DivideTest, DivideByZero) {
EXPECT_THROW(divide(10, 0), std::runtime_error);
}
// Descriptive test names
TEST(UserAuth, LoginFailsWithWrongPassword) { ... }
❌ Don't:
// Don't test multiple things in one test
TEST(EverythingTest, TestAll) {
EXPECT_EQ(add(1, 2), 3);
EXPECT_EQ(subtract(5, 3), 2);
EXPECT_EQ(multiply(2, 3), 6); // If this fails, above already passed
}
// Don't write tests that depend on each other
TEST(OrderTest, Test1) { createFile("test.txt"); }
TEST(OrderTest, Test2) { readFile("test.txt"); } // Depends on Test1!
Quick Reference
CMake Commands
cmake -S . -B build # Generate in 'build' directory
cmake --build build # Build the project
cmake --build build --target clean
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake -DBUILD_TESTS=OFF ..
GDB Commands
gdb ./app # Start debugger
run, r # Run program
break main, b main # Set breakpoint
next, n # Step over
step, s # Step into
continue, c # Continue
print x, p x # Print variable
backtrace, bt # Show call stack
quit, q # Exit
Google Test Commands
./tests # Run all tests
./tests --gtest_filter=Test* # Filter tests
./tests --gtest_list_tests # List tests
./tests --gtest_repeat=10 # Repeat tests
ctest -V # Via CTest
Learning Path
- •Start with CMake - Understanding how to build projects
- •Learn GDB - Debug when things go wrong
- •Master Testing - Prevent bugs before they happen
Each topic includes comprehensive README, examples, and exercises!
Compile & Run Examples
# Navigate to this module
cd 11_Build_Tools_Testing
# Each topic has examples to compile and run
g++ -std=c++17 -Wall -g 01_CMake/examples.cpp -o cmake_demo && ./cmake_demo