Docs

Build Tools Testing

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

  1. Why Build Tools Matter
  2. CMake Essentials
  3. GDB Debugging
  4. Unit Testing with Google Test
  5. Integrating Everything
  6. 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:

  • Makefile on 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

CommandShortDescription
runrStart program
quitqExit GDB
break mainb mainSet breakpoint at main
break file.cpp:42b file.cpp:42Breakpoint at line 42
continuecContinue to next breakpoint
nextnExecute next line (step over)
stepsStep into function
finishfinRun until current function returns
print xp xPrint variable x
backtracebtShow call stack
listlShow source code
info localsShow all local variables
info breakpointsi bList breakpoints
delete 1d 1Delete 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

AssertionFatal VersionDescription
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 -O0 for debugging
  • Use breakpoints instead of print statements
  • Learn TUI mode for visual debugging
  • Use .gdbinit for 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

  1. Start with CMake - Understanding how to build projects
  2. Learn GDB - Debug when things go wrong
  3. 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
Build Tools Testing - C++ Tutorial | DeepML