Home

07 Multi-Language Projects: C++ Code Quality and Testing

lecture cpp python clang-format clang-tidy google-test gcov coverage code-quality

1. Introduction: Building on C++ Fundamentals

In the previous lecture (Part 1), you learned the fundamentals of C++ development:

This lecture focuses on ensuring code quality:

Writing code that compiles is only half the battle—we need code that’s readable, maintainable, and well-tested.

┌─────────────────────────────────────────────────────────────────┐
│                    C++ QUALITY PIPELINE                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Source Code → clang-format → clang-tidy → Google Test → gcov   │
│       │              │              │            │          │   │
│       ▼              ▼              ▼            ▼          ▼   │
│   Your .cpp    Consistent     Bug-free     Verified    Measured │
│   files        style          code         behavior    coverage │
│                                                                 │
│  Just like Python: Ruff → Pyright → pytest → pytest-cov         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

This lecture covers the C++ equivalents to your Python quality tools:

  1. Code formatting — clang-format (like Black/Ruff)
  2. Static analysis — clang-tidy + compiler warnings (like Pyright/Ruff)
  3. Unit testing — Google Test (like pytest)
  4. Coverage reports — gcov/llvm-cov (like pytest-cov)

By the end, you’ll have the same quality assurance for C++ that you already have for Python. Part 3 will then show you how to integrate both languages in a single project.


2. Learning Objectives

By the end of this lecture, you will be able to:

Code Formatting:

  1. Apply clang-format to enforce consistent C++ code style
  2. Configure .clang-format files using the Google C++ Style Guide

Static Analysis:

  1. Configure compiler warnings across GCC, Clang, and MSVC
  2. Use clang-tidy for advanced static analysis and modernization checks

Unit Testing:

  1. Write Google Test tests with TEST(), EXPECT_*, and ASSERT_* macros
  2. Use test fixtures for shared setup and teardown

Coverage:

  1. Generate C++ coverage reports using gcov/llvm-cov
  2. Interpret coverage metrics and identify untested code paths

Comparison:

  1. Map Python quality tools to C++ equivalents (Ruff → clang-format/clang-tidy, pytest → Google Test, pytest-cov → gcov)

3. Code Quality for C++

In Part 1, we learned how to compile C++ code, use CMake for build configuration, and manage dependencies. But writing code that compiles is only half the battle—we also need code that’s readable, maintainable, and bug-free.

You already know this discipline from Python. Remember the road-profile-viewer’s pyproject.toml?

[tool.ruff]
line-length = 120
target-version = "py312"

[tool.ruff.lint]
select = ["E", "W", "F", "I", "N", "B", "C4", "UP"]

[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.12"

This configuration enforces:

Now we need the same discipline for C++. But there’s a complication…

3.1 The Problem: C++ Has No PEP 8

In Python, code quality is remarkably standardized:

┌─────────────────────────────────────────────────────────────────────┐
│                    PYTHON CODE QUALITY ECOSYSTEM                    │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Style Guide:      PEP 8 (the standard, everyone agrees)            │
│                           │                                         │
│                           ▼                                         │
│  Tool:             Ruff (or Black + Flake8 + isort)                 │
│                           │                                         │
│                           ▼                                         │
│  Configuration:    pyproject.toml (one file for everything)         │
│                           │                                         │
│                           ▼                                         │
│  Result:           Consistent code across all Python projects       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

C++ is more fragmented:

┌─────────────────────────────────────────────────────────────────────┐
│                    C++ CODE QUALITY ECOSYSTEM                       │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Style Guides:     Google Style    LLVM Style    Mozilla Style      │
│                    GNU Style       Chromium      Linux Kernel       │
│                    MISRA C++       (and dozens more...)             │
│                           │                                         │
│                           ▼                                         │
│  Tools:            clang-format (formatting)                        │
│                    clang-tidy (linting)                             │
│                    cppcheck, cpplint, PVS-Studio, ...               │
│                           │                                         │
│                           ▼                                         │
│  Configuration:    .clang-format + .clang-tidy (separate files)     │
│                    No single standard configuration                 │
│                           │                                         │
│                           ▼                                         │
│  Result:           Every project looks different                    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Why this matters:

When you clone a new Python project, you can immediately read the code—it all looks like PEP 8. When you clone a C++ project, you might find:

The good news: The C++ community has converged on two excellent tools from the LLVM project:

  1. clang-format — Automatic code formatting (like ruff format)
  2. clang-tidy — Static analysis and linting (like ruff check)

With proper configuration, you can achieve the same consistency in C++ that you’re used to in Python.

3.2 The Google C++ Style Guide

For this course, we’ll use the Google C++ Style Guide—one of the most widely adopted standards. Google open-sourced their internal style guide, and it’s now used by:

Why Google Style for this course?

Reason Explanation
Well-documented The style guide explains why each rule exists, not just what to do
Tool support clang-format has built-in BasedOnStyle: Google
Python-friendly Uses snake_case for functions/variables (like Python!)
Modern C++ Updated for C++11/14/17/20 features
Industry adoption Knowing Google Style is useful for your career

3.3 Naming Conventions: Python vs. Google C++

Here’s how naming conventions compare:

Element Python (PEP 8) Google C++ Example
Functions snake_case snake_case calculate_distance()
Variables snake_case snake_case ray_angle
Classes PascalCase PascalCase RoadProfile
Constants ALL_CAPS kPascalCase Python: MAX_SPEED
C++: kMaxSpeed
Private members _leading_underscore trailing_underscore_ Python: self._data
C++: data_

Notice: Functions and variables use the same convention! This is intentional—Google’s style guide was influenced by Python.

// Google C++ Style - feels familiar to Python developers
class RoadProfile {
 public:
  double calculate_elevation(double x_position) const;

 private:
  std::vector<double> elevation_data_;  // trailing underscore for private
};

// compare to Python:
// class RoadProfile:
//     def calculate_elevation(self, x_position: float) -> float:
//
//     _elevation_data: list[float]  # leading underscore for private

3.4 Formatting Conventions

Some key differences from Python:

// Google C++ Style

// 2-space indentation (not 4 like Python)
if (condition) {
  do_something();
  do_something_else();
}

// Opening brace on same line (like Python's ":" at end of line)
void calculate_ray_line(double angle) {
  // implementation
}

// Line length: 80 characters (configurable to 100/120)
// This matches what you configured in pyproject.toml: line-length = 120

The philosophy is the same: Consistent formatting makes code easier to read. The specific rules differ slightly, but the discipline is identical.


4. clang-format: The C++ Formatter

In Python, you run ruff format (or black) to automatically format your code. In C++, the equivalent is clang-format.

4.1 How clang-format Relates to Ruff

Think of clang-format as “Ruff format for C++”:

Aspect Python (Ruff format) C++ (clang-format)
Purpose Auto-format Python code to PEP 8 Auto-format C++ code to chosen style
Config file pyproject.toml or ruff.toml .clang-format
Format in place ruff format . clang-format -i file.cpp
Check only (CI) ruff format --check . clang-format --dry-run --Werror file.cpp
Base style PEP 8 (implicit) BasedOnStyle: Google (explicit)

4.2 Configuration Comparison: pyproject.toml vs .clang-format

Let’s compare the formatting configuration from your road-profile-viewer with an equivalent C++ configuration:

Python (pyproject.toml):

[tool.ruff]
line-length = 120
target-version = "py312"

[tool.ruff.format]
quote-style = "double"
line-ending = "lf"
indent-style = "space"
docstring-code-format = true

C++ (.clang-format):

# .clang-format
---
Language: Cpp
BasedOnStyle: Google

# Line length (like line-length = 120)
ColumnLimit: 100

# Indentation (like indent-style = "space")
IndentWidth: 2
UseTab: Never

# Quote style doesn't apply (C++ uses "" for strings, '' for chars)
# But we can control include style:
IncludeBlocks: Regroup

# Brace style
BreakBeforeBraces: Attach

# Short forms
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false

# Template formatting
AlwaysBreakTemplateDeclarations: Yes

# Include ordering (like isort in Python)
IncludeCategories:
  - Regex: '^<.*\.h>'     # C headers first
    Priority: 1
  - Regex: '^<.*>'        # C++ standard library
    Priority: 2
  - Regex: '^".*"'        # Project headers last
    Priority: 3
...

Key insight: Both configurations solve the same problems—they just use different syntax. The IncludeCategories section in clang-format is directly analogous to what isort (now part of Ruff) does for Python imports.

4.3 Installing clang-format

# Ubuntu/Debian
sudo apt install clang-format

# macOS
brew install clang-format

# Windows (with LLVM - includes clang-format and clang-tidy)
choco install llvm
# Or with winget:
winget install LLVM.LLVM

Verify installation:

clang-format --version
# Output: clang-format version 17.0.6 (or similar)

4.4 Running clang-format

The workflow mirrors what you do with Ruff:

Format a single file (like ruff format file.py):

clang-format -i src/geometry.cpp

The -i flag means “in-place” (modify the file). Without it, clang-format prints the formatted code to stdout.

Check without modifying (for CI, like ruff format --check):

clang-format --dry-run --Werror src/geometry.cpp

Format all C++ files (like ruff format .):

# Linux/macOS
find . -name '*.cpp' -o -name '*.hpp' | xargs clang-format -i

# Or use a more portable approach with CMake (we'll see this in CI)

4.5 Before and After: clang-format in Action

Before formatting:

#include "geometry.hpp"
#include <cmath>
#include <vector>
#include <iostream>

double calculate_ray_line(double angle,double camera_x,double camera_y,double x_max){
double angle_rad=-angle*3.14159265359/180.0;
double slope=std::tan(angle_rad);
if(std::abs(std::cos(angle_rad))<1e-10){return 0.0;}
double x_end=x_max;
return slope*(x_end-camera_x)+camera_y;
}

After clang-format -i with Google style:

#include <cmath>
#include <iostream>
#include <vector>

#include "geometry.hpp"

double calculate_ray_line(double angle, double camera_x, double camera_y,
                          double x_max) {
  double angle_rad = -angle * 3.14159265359 / 180.0;
  double slope = std::tan(angle_rad);
  if (std::abs(std::cos(angle_rad)) < 1e-10) {
    return 0.0;
  }
  double x_end = x_max;
  return slope * (x_end - camera_x) + camera_y;
}

What changed:


5. clang-tidy: The C++ Linter

While clang-format handles how your code looks, clang-tidy analyzes what your code does. It catches bugs, suggests improvements, and enforces best practices—just like ruff check does for Python.

5.1 How clang-tidy Relates to Ruff Check

Remember the Ruff configuration in your road-profile-viewer?

[tool.ruff.lint]
select = ["E", "W", "F", "I", "N", "B", "C4", "UP"]

Each letter is a category of checks:

clang-tidy works the same way—it has categories of checks you enable or disable:

Python (Ruff) C++ (clang-tidy) What It Catches
F (Pyflakes) bugprone-* Likely bugs, undefined behavior
B (Bugbear) bugprone-* Subtle bugs, dangerous patterns
C4 (Comprehensions) performance-* Performance improvements
UP (pyupgrade) modernize-* Use modern language features
N (pep8-naming) readability-identifier-naming Naming conventions
E, W (pycodestyle) readability-* Style and clarity
(No direct equivalent) cppcoreguidelines-* C++ Core Guidelines (safety)

5.2 Configuration Comparison: pyproject.toml vs .clang-tidy

Python (pyproject.toml):

[tool.ruff.lint]
# Select which rule categories to enable
select = ["E", "W", "F", "I", "N", "B", "C4", "UP"]

# Make all rules auto-fixable
fixable = ["ALL"]

# Ignore specific rules
ignore = ["E501"]  # Ignore line length (handled by formatter)

C++ (.clang-tidy):

# .clang-tidy
---
# Enable check categories (like select = [...])
Checks: >
  -*,
  bugprone-*,
  performance-*,
  modernize-*,
  readability-*,
  -modernize-use-trailing-return-type,
  -readability-magic-numbers

# Treat specific warnings as errors (like --error-on in Ruff)
WarningsAsErrors: ''

# Configure specific checks (like per-rule settings in Ruff)
CheckOptions:
  - key: readability-identifier-naming.NamespaceCase
    value: lower_case
  - key: readability-identifier-naming.ClassCase
    value: CamelCase
  - key: readability-identifier-naming.FunctionCase
    value: lower_case
  - key: readability-identifier-naming.VariableCase
    value: lower_case
...

The pattern is identical:

  1. Start by disabling all checks (-* or start with empty selection)
  2. Enable categories you want (bugprone-* or "F")
  3. Disable specific annoying rules (-readability-magic-numbers or ignore = ["E501"])

5.3 Why clang-tidy Needs compile_commands.json

Here’s an important difference: clang-tidy needs to understand your project structure.

Python (Ruff): Can analyze any .py file immediately—Python’s import system is standardized.

C++ (clang-tidy): Needs to know:

CMake generates a compile_commands.json file that contains all this information:

# Generate compile_commands.json
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

# This creates build/compile_commands.json:
# [
#   {
#     "directory": "/path/to/project/build",
#     "command": "g++ -std=c++20 -I../include -c ../src/geometry.cpp",
#     "file": "../src/geometry.cpp"
#   },
#   ...
# ]

Now clang-tidy knows exactly how each file should be compiled, and can analyze it correctly.

5.4 Running clang-tidy

Step 1: Generate compilation database:

cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

Step 2: Run on a single file (like ruff check file.py):

clang-tidy -p build src/geometry.cpp

The -p build flag tells clang-tidy where to find compile_commands.json.

Step 3: Run on all source files (like ruff check .):

# Linux/macOS
find src -name '*.cpp' | xargs clang-tidy -p build

# Or use run-clang-tidy (parallel, faster)
run-clang-tidy -p build src/

5.5 Example: What clang-tidy Catches

Your code:

void process_data(std::vector<double> data) {  // Pass by value (expensive copy!)
    for (int i = 0; i < data.size(); i++) {    // Signed/unsigned comparison
        if (data[i] = 0.0) {                   // Assignment instead of comparison!
            continue;
        }
    }
}

clang-tidy output:

geometry.cpp:1:25: warning: parameter 'data' is passed by value; consider
                  passing by const reference [performance-unnecessary-value-param]
geometry.cpp:2:21: warning: comparison of integers of different signs
                  [bugprone-signed-unsigned-comparison]
geometry.cpp:3:21: warning: using the result of an assignment as a condition
                  without parentheses [bugprone-assignment-in-if-condition]

Compare to Ruff catching similar issues in Python:

def process_data(data):
    for i in range(len(data)):  # Ruff: Use enumerate() instead [C416]
        if data[i] = 0.0:       # Python: SyntaxError (caught by interpreter)
            continue

The philosophy is the same: catch bugs before they reach production.

5.6 VS Code Integration: clangd Extension

In Python, you installed the Ruff extension for VS Code to get real-time formatting and linting. For C++, the equivalent is clangd—a language server that brings the power of clang-format and clang-tidy directly into your IDE.

Installing clangd:

  1. Open VS Code Extensions (Ctrl+Shift+X)
  2. Search for “clangd” (by LLVM)
  3. Click Install

What clangd provides:

🐍 Ruff = clangd
Feature 🐍Python (Ruff) C++ (clangd)
Format on save
Automatic
Automatic
Real-time linting
Squiggly lines
Squiggly lines
Quick fixes
Code actions
Code actions
Go to definition
Pylance/Pyright
clangd
Hover documentation
Type hints
Type info + docs

VS Code settings comparison:

Python (settings.json):

{
  // Ruff as the formatter
  "[python]": {
    "editor.defaultFormatter": "charliermarsh.ruff",
    "editor.formatOnSave": true
  },
  // Enable Ruff linting
  "ruff.lint.enable": true
}

C++ (settings.json):

{
  // clangd as the formatter (uses .clang-format)
  "[cpp]": {
    "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd",
    "editor.formatOnSave": true
  },
  // clangd settings
  "clangd.arguments": [
    "--background-index",
    "--clang-tidy",
    "--header-insertion=never"
  ]
}

Key clangd arguments:

Argument Purpose Ruff Equivalent
--clang-tidy Enable real-time linting ruff.lint.enable: true
--background-index Index project for fast navigation (Pylance does this automatically)
--header-insertion=never Don't auto-add includes (N/A for Python)
--compile-commands-dir=build Where to find compile_commands.json (Python doesn't need this)

The workflow is identical:

  1. Write code → clangd shows real-time warnings (yellow/red squiggles)
  2. Save file → clang-format automatically formats
  3. Hover over warning → See explanation and suggested fix
  4. Click lightbulb → Apply automatic fix

Recommended extensions for C++ development:

Extension Purpose Python Equivalent
clangd (LLVM) Formatting, linting, navigation Ruff + Pylance
CMake Tools Build, debug, configure CMake (uv/pip are simpler)
C/C++ Extension Pack Debugger, additional tools Python Debugger

Important: If you have the Microsoft C/C++ extension installed, you may need to disable its IntelliSense to avoid conflicts with clangd. Add to settings.json:

{
  "C_Cpp.intelliSenseEngine": "disabled"
}

The key insight: Just as you configured Ruff in VS Code to format and lint Python on save, clangd does the same for C++. The configuration files are different (.clang-format + .clang-tidy vs pyproject.toml), but the developer experience is identical.


6. Static Code Analysis

Static analysis examines code without running it. In Python, this was limited because Python is dynamically typed. In C++, static analysis is more powerful because of static typing.

6.1 Why Static Code Analysis?

Before diving into tools, let’s understand why static analysis matters:

1. Catch Bugs Before Runtime

Static analysis finds issues that would otherwise only appear at runtime (or worse, in production):

Issue Type Without Static Analysis With Static Analysis
Null pointer dereference Crash in production Caught at compile time
Buffer overflow Security vulnerability Warning before merge
Resource leak Memory grows over time Detected in PR review
Type confusion Undefined behavior Compiler error

2. Reduce Code Review Burden

When static analysis catches style issues and obvious bugs automatically, human reviewers can focus on:

3. Enforce Consistency Across Team

Without automated checks, code style depends on who reviews the PR. With static analysis:

4. Cost of Fixing Bugs Increases Over Time

Cost to fix a bug:
  During coding:     1x (you're already there)
  During code review: 5x (context switch for reviewer)
  During testing:    10x (write test, debug, fix)
  In production:     100x (incident response, customer impact)

Static analysis catches bugs at the 1x cost stage.

6.2 CI Integration: The Key to Effective Analysis

Having static analysis tools installed locally is useful, but the real power comes from integrating them into your CI pipeline. The key principle:

Every merged PR must pass static analysis. No exceptions.

Why CI Integration is Essential:

  1. Developers can’t skip it - Local checks can be bypassed; CI cannot
  2. Consistent environment - Same compiler version, same flags, same tools
  3. Visible to everyone - PR status shows pass/fail for all reviewers
  4. Historical tracking - See quality trends over time

Python CI Example (from Lecture 2):

# .github/workflows/ci.yml
name: Python Quality
on: [push, pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install uv
        uses: astral-sh/setup-uv@v4
      - name: Run Ruff linter
        run: uv run ruff check src/
      - name: Run Ruff formatter check
        run: uv run ruff format --check src/
      - name: Run Pyright type checker
        run: uv run pyright src/

C++ CI Example:

# .github/workflows/cpp-ci.yml
name: C++ Quality
on: [push, pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure CMake
        run: cmake -B build -DCMAKE_BUILD_TYPE=Release

      - name: Build with warnings as errors
        run: cmake --build build -- -j$(nproc)
        # Build fails if any warning occurs (-Werror)

      - name: Run clang-tidy
        run: |
          find src -name '*.cpp' | xargs clang-tidy \
            -p build \
            --warnings-as-errors='*'

      - name: Check formatting
        run: |
          find src -name '*.cpp' -o -name '*.hpp' | xargs clang-format --dry-run --Werror

The PR Gate Pattern:

Developer pushes code
        ↓
CI runs automatically
        ↓
    ┌───────────────────────────────────┐
    │  ✅ Build passes                  │
    │  ✅ clang-format check passes     │
    │  ✅ clang-tidy check passes       │
    │  ✅ All tests pass                │
    └───────────────────────────────────┘
        ↓
PR can be merged (green checkmark)

If any check fails, the PR is blocked until fixed.

6.3 Monitoring and Dashboards

Running checks in CI is step one. Step two is making results visible and actionable.

GitHub Actions: Inline Annotations

GitHub Actions can show warnings directly in the PR diff:

- name: Run clang-tidy with annotations
  run: |
    clang-tidy src/*.cpp 2>&1 | \
    sed -E 's/^(.+):([0-9]+):([0-9]+): (warning|error): (.+)/::error file=\1,line=\2,col=\3::\5/'

This produces inline annotations:

src/geometry.cpp
Line 42: ⚠️ warning: variable 'x' is not initialized [cppcoreguidelines-init-variables]

SonarQube/SonarCloud: Quality Gates

For larger projects, dedicated platforms provide dashboards:

Metric Description Typical Threshold
Code Coverage Percentage of code executed by tests > 80%
Duplicated Lines Copy-pasted code blocks < 3%
Code Smells Maintainability issues A rating (best)
Bugs Likely runtime errors 0 new bugs
Vulnerabilities Security issues 0 new vulnerabilities

Codecov: Coverage Tracking

- name: Generate coverage report
  run: |
    cmake --build build --target coverage
    # Generates lcov.info

- name: Upload to Codecov
  uses: codecov/codecov-action@v4
  with:
    files: build/coverage/lcov.info
    fail_ci_if_error: true

Codecov shows:

GitHub Status Checks: The Bottom Line

All these tools can report status to GitHub:

Pull Request #42: Add intersection calculation
┌────────────────────────────────────────────┐
│ ✅ build (ubuntu-latest)          2m 34s   │
│ ✅ clang-tidy                     1m 12s   │
│ ✅ clang-format                   0m 08s   │
│ ✅ tests (ubuntu-latest)          0m 45s   │
│ ✅ codecov/patch                  80.5%    │
│ ✅ codecov/project                85.2%    │
└────────────────────────────────────────────┘
All checks have passed

Comparison: Python vs C++ Monitoring

Aspect Python C++
Linting Ruff (fast, integrated) clang-tidy (comprehensive)
Formatting Ruff format clang-format
Type checking Pyright Compiler (built-in)
Coverage pytest-cov gcov / llvm-cov
CI integration GitHub Actions + Codecov GitHub Actions + Codecov

6.4 Compiler Warnings as Analysis

The C++ compiler itself is your first static analyzer. Different compilers have different warning options.

6.5 Compiler Warning Options

GCC:

# Basic warnings (recommended minimum)
g++ -Wall -Wextra -c main.cpp

# Pedantic (strict standard compliance)
g++ -Wall -Wextra -Wpedantic -c main.cpp

# Warnings as errors (CI configuration)
g++ -Wall -Wextra -Wpedantic -Werror -c main.cpp

Clang:

# Recommended Clang warnings
clang++ -Wall -Wextra -Wpedantic -c main.cpp

MSVC (Windows):

# Warning level 4 (highest recommended)
cl /W4 /WX main.cpp

6.6 Compiler Warning Summary

Purpose GCC Clang MSVC
Standard warnings -Wall -Wextra -Wall -Wextra /W4
Strict standards -Wpedantic -Wpedantic /permissive-
Warnings as errors -Werror -Werror /WX

6.7 CMake Configuration for Compiler Warnings

# CMakeLists.txt
if(MSVC)
    add_compile_options(/W4 /WX)
else()
    add_compile_options(-Wall -Wextra -Wpedantic -Werror)
endif()

7. Testing Frameworks for C++

In Python, you use pytest. In C++, the equivalent is Google Test (gtest). Just as Ruff is the de facto Python linter, Google Test is the de facto C++ testing framework.

7.1 How Google Test Relates to pytest

Think of Google Test as “pytest for C++”:

Aspect Python (pytest) C++ (Google Test)
Purpose Run tests, report failures Run tests, report failures
Installation uv add pytest --dev CMake FetchContent
Config file pyproject.toml CMakeLists.txt
Run tests pytest ctest
Verbose output pytest -v ctest --output-on-failure
Run specific test pytest -k "test_name" ctest -R "test_name"
Maintainer Community Google

Google Test is:

7.2 Installation Comparison

Python: Add to dev dependencies in pyproject.toml:

uv add pytest --dev

C++: Add to CMakeLists.txt using FetchContent:

# CMakeLists.txt
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

enable_testing()

add_executable(tests
    tests/test_geometry.cpp
)
target_link_libraries(tests PRIVATE
    geometry_core
    GTest::gtest_main
)

include(GoogleTest)
gtest_discover_tests(tests)

Key insight: Both use a “declare dependencies, let the tool fetch them” pattern. uv sync downloads pytest; cmake downloads Google Test.

7.3 Test Structure Comparison

Side-by-side comparison of equivalent tests:

Python (tests/test_geometry.py):

import pytest
from road_profile_viewer.geometry import calculate_ray_line

def test_calculate_ray_line_basic():
    """Test basic ray calculation."""
    result = calculate_ray_line(45.0, 0.0, 2.0, 80.0)

    assert len(result["x"]) == 2
    assert len(result["y"]) == 2

def test_calculate_ray_line_horizontal():
    """Test horizontal ray (0 degrees)."""
    result = calculate_ray_line(0.0, 0.0, 2.0, 80.0)

    # Horizontal ray: y stays constant
    assert result["y"][0] == result["y"][1]

C++ (tests/test_geometry.cpp):

#include <gtest/gtest.h>
#include "geometry.hpp"

namespace rpv = road_profile_viewer;

// Test basic ray calculation
TEST(GeometryTest, CalculateRayLineBasic) {
    auto result = rpv::calculate_ray_line(45.0, 0.0, 2.0, 80.0);

    EXPECT_EQ(result.x.size(), 2);
    EXPECT_EQ(result.y.size(), 2);
}

// Test horizontal ray (0 degrees)
TEST(GeometryTest, CalculateRayLineHorizontal) {
    auto result = rpv::calculate_ray_line(0.0, 0.0, 2.0, 80.0);

    // Horizontal ray: y stays constant
    EXPECT_DOUBLE_EQ(result.y[0], result.y[1]);
}

Pattern mapping:

7.4 Assertions and Expectations

pytest Google Test Behavior on Failure
assert x == y EXPECT_EQ(x, y) Continue test
assert x == y (critical) ASSERT_EQ(x, y) Stop test immediately
assert x != y EXPECT_NE(x, y) Continue test
assert x < y EXPECT_LT(x, y) Continue test
assert x > y EXPECT_GT(x, y) Continue test
assert abs(x - y) < 0.001 EXPECT_NEAR(x, y, 0.001) Continue test
assert x == pytest.approx(y) EXPECT_DOUBLE_EQ(x, y) Continue test
assert x is None EXPECT_FALSE(x.has_value()) Continue test
assert x is not None ASSERT_TRUE(x.has_value()) Stop if None (can't continue)

EXPECT vs ASSERT - The Key Difference:

In pytest, every assert stops the test on failure. Google Test gives you a choice:

TEST(GeometryTest, FindIntersection) {
    auto result = rpv::find_intersection(x_road, y_road, 10.0);

    // ASSERT: Must pass before we can access result->x
    ASSERT_TRUE(result.has_value());  // Stop if None

    // EXPECT: Can check multiple properties
    EXPECT_GT(result->x, 0.0);        // Continue even if fails
    EXPECT_GT(result->distance, 0.0); // Still runs
}

7.5 Test Fixtures Comparison

Both pytest and Google Test support fixtures for shared setup.

Python (pytest fixtures):

import pytest
from road_profile_viewer.geometry import find_intersection

@pytest.fixture
def road_profile():
    """Shared test data."""
    return {
        "x": [0, 10, 20, 30, 40],
        "y": [0, 2, 4, 3, 5]
    }

def test_find_intersection_exists(road_profile):
    result = find_intersection(
        road_profile["x"],
        road_profile["y"],
        angle=10.0
    )

    assert result is not None
    assert result["distance"] > 0

def test_find_intersection_no_hit(road_profile):
    # Ray pointing up, never hits road
    result = find_intersection(
        road_profile["x"],
        road_profile["y"],
        angle=-45.0  # Pointing upward
    )

    assert result is None

C++ (Google Test fixtures):

class GeometryTest : public ::testing::Test {
protected:
    void SetUp() override {
        // Shared test data (like @pytest.fixture)
        x_road = {0, 10, 20, 30, 40};
        y_road = {0, 2, 4, 3, 5};
    }

    // Fixture data available to all tests
    std::vector<double> x_road;
    std::vector<double> y_road;
};

// Use TEST_F (F = Fixture) instead of TEST
TEST_F(GeometryTest, FindIntersectionExists) {
    auto result = rpv::find_intersection(
        x_road, y_road, 10.0, 0.0, 10.0
    );

    ASSERT_TRUE(result.has_value());
    EXPECT_GT(result->distance, 0.0);
}

TEST_F(GeometryTest, FindIntersectionNoHit) {
    // Ray pointing up, never hits road
    auto result = rpv::find_intersection(
        x_road, y_road, -45.0, 0.0, 10.0
    );

    EXPECT_FALSE(result.has_value());
}

Pattern mapping:

7.6 Parametrized Tests Comparison

Both frameworks support running the same test with different inputs.

Python (@pytest.mark.parametrize):

import pytest

@pytest.mark.parametrize("angle,expected_hits", [
    (10.0, True),    # Shallow angle, hits road
    (45.0, True),    # Steep angle, hits road
    (-10.0, False),  # Upward angle, misses
    (90.0, False),   # Vertical, edge case
])
def test_intersection_angles(road_profile, angle, expected_hits):
    result = find_intersection(
        road_profile["x"],
        road_profile["y"],
        angle=angle
    )

    assert (result is not None) == expected_hits

C++ (Google Test INSTANTIATE_TEST_SUITE_P):

struct AngleTestCase {
    double angle;
    bool expected_hits;
};

class AngleTest : public ::testing::TestWithParam<AngleTestCase> {
protected:
    void SetUp() override {
        x_road = {0, 10, 20, 30, 40};
        y_road = {0, 2, 4, 3, 5};
    }
    std::vector<double> x_road;
    std::vector<double> y_road;
};

TEST_P(AngleTest, IntersectionAngles) {
    auto [angle, expected_hits] = GetParam();

    auto result = rpv::find_intersection(
        x_road, y_road, angle, 0.0, 10.0
    );

    EXPECT_EQ(result.has_value(), expected_hits);
}

INSTANTIATE_TEST_SUITE_P(
    GeometryAngles,
    AngleTest,
    ::testing::Values(
        AngleTestCase{10.0, true},    // Shallow angle
        AngleTestCase{45.0, true},    // Steep angle
        AngleTestCase{-10.0, false},  // Upward angle
        AngleTestCase{90.0, false}    // Vertical
    )
);

Key insight: The concept is identical—run the same test logic with different data. pytest is more concise; Google Test is more explicit.

7.7 Running Tests Comparison

Action Python (pytest) C++ (ctest/gtest)
Run all tests pytest ctest --test-dir build
Verbose output pytest -v ctest --output-on-failure
Run by name pattern pytest -k "ray" ctest -R "ray"
Run specific file pytest tests/test_geometry.py ./build/tests --gtest_filter="Geometry*"
Stop on first failure pytest -x ctest --stop-on-failure
Parallel execution pytest -n auto ctest -j$(nproc)

Build and run workflow:

# Python
uv run pytest -v

# C++
cmake --build build
ctest --test-dir build --output-on-failure

# Or run the test executable directly for more details
./build/tests

7.8 Test Output Comparison

pytest output:

========================= test session starts ==========================
collected 4 items

tests/test_geometry.py::test_calculate_ray_line_basic PASSED     [ 25%]
tests/test_geometry.py::test_calculate_ray_line_horizontal PASSED [ 50%]
tests/test_geometry.py::test_find_intersection_exists PASSED      [ 75%]
tests/test_geometry.py::test_find_intersection_no_hit PASSED      [100%]

========================== 4 passed in 0.12s ===========================

Google Test output:

[==========] Running 4 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 4 tests from GeometryTest
[ RUN      ] GeometryTest.CalculateRayLineBasic
[       OK ] GeometryTest.CalculateRayLineBasic (0 ms)
[ RUN      ] GeometryTest.CalculateRayLineHorizontal
[       OK ] GeometryTest.CalculateRayLineHorizontal (0 ms)
[ RUN      ] GeometryTest.FindIntersectionExists
[       OK ] GeometryTest.FindIntersectionExists (0 ms)
[ RUN      ] GeometryTest.FindIntersectionNoHit
[       OK ] GeometryTest.FindIntersectionNoHit (0 ms)
[----------] 4 tests from GeometryTest (0 ms total)

[==========] 4 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 4 tests.

The output format differs, but the information is the same: Which tests ran, which passed, which failed, and how long it took.


8. Coverage Reports for C++

In Python, you use pytest-cov to measure test coverage. In C++, the equivalent tools are gcov (GCC) and llvm-cov (Clang). The workflow is the same: instrument code, run tests, generate report.

8.1 How C++ Coverage Relates to pytest-cov

Aspect Python (pytest-cov) C++ (gcov/llvm-cov)
Installation uv add pytest-cov --dev Built into GCC/Clang
Enable coverage pytest --cov=src CMake: -DENABLE_COVERAGE=ON
Run with coverage pytest --cov=src ctest (after coverage build)
Terminal report --cov-report=term gcov or llvm-cov report
HTML report --cov-report=html genhtml (lcov) or llvm-cov show
CI upload Codecov action Codecov action (same!)

8.2 Coverage Workflow Comparison

Python workflow:

# Install
uv add pytest-cov --dev

# Run tests with coverage
uv run pytest --cov=src --cov-report=term --cov-report=html

# View HTML report
open htmlcov/index.html

C++ workflow:

# Configure with coverage enabled
cmake -B build -DENABLE_COVERAGE=ON

# Build
cmake --build build

# Run tests (this generates coverage data)
ctest --test-dir build

# Generate HTML report
lcov --capture --directory build --output-file coverage.info
genhtml coverage.info --output-directory htmlcov

# View HTML report
open htmlcov/index.html

Key insight: The steps are conceptually identical:

  1. Enable coverage instrumentation
  2. Run tests
  3. Generate human-readable report

8.3 CMake Coverage Configuration

Add this to your CMakeLists.txt:

# CMakeLists.txt
option(ENABLE_COVERAGE "Enable code coverage" OFF)

if(ENABLE_COVERAGE)
    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
        # Add coverage flags (like pytest --cov)
        add_compile_options(--coverage -O0 -g)
        add_link_options(--coverage)
    endif()
endif()

Comparison with pyproject.toml:

# pyproject.toml (Python)
[tool.pytest.ini_options]
addopts = "--cov=src --cov-report=term"

Both configure coverage in the project’s build configuration file.

8.4 gcov (GCC) vs llvm-cov (Clang)

Aspect gcov (GCC) llvm-cov (Clang)
Compiler GCC (g++) Clang (clang++)
Coverage flags --coverage -fprofile-instr-generate -fcoverage-mapping
Raw data format .gcda, .gcno .profraw
HTML generator lcov + genhtml llvm-cov show --format=html
CI compatibility Codecov, Coveralls Codecov, Coveralls

gcov workflow (more common in Linux):

# Build with GCC coverage
cmake -B build -DCMAKE_CXX_COMPILER=g++ -DENABLE_COVERAGE=ON
cmake --build build

# Run tests
ctest --test-dir build

# Generate lcov report
lcov --capture --directory build --output-file coverage.info
lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage.info

# Generate HTML
genhtml coverage.info --output-directory htmlcov

llvm-cov workflow (better source annotation):

# Build with Clang coverage
cmake -B build -DCMAKE_CXX_COMPILER=clang++ -DENABLE_COVERAGE=ON
cmake --build build

# Run tests (generates .profraw)
ctest --test-dir build

# Merge profile data
llvm-profdata merge -sparse default.profraw -o coverage.profdata

# Generate report
llvm-cov show ./build/tests -instr-profile=coverage.profdata --format=html > htmlcov/index.html

8.5 Coverage Report Comparison

pytest-cov terminal output:

---------- coverage: platform linux, python 3.12.0 ----------
Name                              Stmts   Miss  Cover
-----------------------------------------------------
src/road_profile_viewer/__init__.py     2      0   100%
src/road_profile_viewer/geometry.py    45      3    93%
src/road_profile_viewer/visualization.py   78     12    85%
-----------------------------------------------------
TOTAL                               125     15    88%

gcov/lcov terminal output:

Reading tracefile coverage.info
Summary coverage rate:
  lines......: 88.0% (110 of 125 lines)
  functions..: 100.0% (12 of 12 functions)
  branches...: 75.0% (30 of 40 branches)

Both show the same key metric: What percentage of your code was executed by tests.

8.6 CI Integration Comparison

Python CI (.github/workflows/python-ci.yml):

- name: Run tests with coverage
  run: uv run pytest --cov=src --cov-report=xml

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v4
  with:
    files: coverage.xml
    fail_ci_if_error: true

C++ CI (.github/workflows/cpp-ci.yml):

- name: Configure with coverage
  run: cmake -B build -DENABLE_COVERAGE=ON

- name: Build
  run: cmake --build build

- name: Run tests
  run: ctest --test-dir build

- name: Generate coverage report
  run: |
    lcov --capture --directory build --output-file coverage.info
    lcov --remove coverage.info '/usr/*' '*/tests/*' -o coverage.info

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v4
  with:
    files: coverage.info
    fail_ci_if_error: true

Key insight: Both upload to the same Codecov service. Your Python and C++ coverage reports appear on the same dashboard, making it easy to track overall project health.

8.7 Coverage Thresholds

Python (in pyproject.toml or pytest.ini):

[tool.coverage.report]
fail_under = 80

C++ (in CI workflow):

- name: Check coverage threshold
  run: |
    COVERAGE=$(lcov --summary coverage.info | grep "lines" | grep -oP '\d+\.\d+')
    if (( $(echo "$COVERAGE < 80" | bc -l) )); then
      echo "Coverage $COVERAGE% is below 80% threshold"
      exit 1
    fi

Or use Codecov’s built-in threshold:

# codecov.yml (same for Python and C++)
coverage:
  status:
    project:
      default:
        target: 80%
        threshold: 1%

8.8 The Coverage Workflow Summary

Coverage: Same Concept, Different Tools
Python
uv add pytest-cov
pytest --cov
coverage.xml
📊
Codecov
C++
cmake -DCOVERAGE=ON
ctest
lcov→ coverage.info

9. Comparison: Python vs. C++ Quality Tools

Now that we’ve covered code quality, testing, and coverage for both ecosystems, let’s put it all together. This comprehensive comparison shows how the discipline you learned in Python transfers directly to C++.

9.1 Tool Comparison

Function Python C++
Style Guide PEP 8 Google C++ Style Guide
Formatter Ruff format, Black clang-format
Linter Ruff check, Pylint clang-tidy
Type Checker Pyright, mypy Compiler (built-in)
Config File pyproject.toml .clang-format + .clang-tidy
Import Sorting isort (part of Ruff) clang-format IncludeCategories
Pre-commit Hook ruff check --fix && ruff format clang-format -i && clang-tidy

9.2 Workflow Comparison

Workflow Step Python C++
Format all files ruff format . find . -name '*.cpp' | xargs clang-format -i
Check formatting (CI) ruff format --check . clang-format --dry-run --Werror
Run linter ruff check . clang-tidy -p build src/*.cpp
Fix linter issues ruff check --fix . clang-tidy -p build --fix src/*.cpp
Type check pyright . (Compiler does this automatically)
IDE integration VS Code Python extension VS Code C/C++ extension, clangd

9.3 Configuration Philosophy

Aspect Python C++
Config location One file: pyproject.toml Separate files per tool
Config format TOML YAML (clang tools)
Base style PEP 8 (implicit default) BasedOnStyle: Google (explicit)
Rule selection select = ["E", "W", "F"] Checks: bugprone-*, performance-*
Disable rules ignore = ["E501"] -readability-magic-numbers

9.4 CI/CD Integration

CI Step Python (GitHub Actions) C++ (GitHub Actions)
Setup uses: astral-sh/setup-uv@v4 sudo apt install clang-format clang-tidy
Format check uv run ruff format --check . clang-format --dry-run --Werror src/*.cpp
Lint check uv run ruff check . clang-tidy -p build src/*.cpp
Type check uv run pyright (Part of cmake --build)
Exit on failure Automatic (non-zero exit) --warnings-as-errors='*'

9.5 Testing Comparison

Aspect Python (pytest) C++ (Google Test)
Install uv add pytest --dev CMake FetchContent
Run tests pytest ctest --test-dir build
Verbose pytest -v ctest --output-on-failure
Filter tests pytest -k "ray" ctest -R "ray"
Assertions assert x == y EXPECT_EQ(x, y)
Fixtures @pytest.fixture class : public ::testing::Test
Parametrize @pytest.mark.parametrize INSTANTIATE_TEST_SUITE_P

9.6 Coverage Comparison

Aspect Python (pytest-cov) C++ (gcov/llvm-cov)
Install uv add pytest-cov --dev Built into GCC/Clang
Enable pytest --cov=src cmake -DENABLE_COVERAGE=ON
HTML report --cov-report=html genhtml coverage.info
CI upload codecov/codecov-action@v4 codecov/codecov-action@v4
Threshold fail_under = 80 codecov.yml: target: 80%

9.7 The Key Insight

┌─────────────────────────────────────────────────────────────────────┐
│                    THE SAME DISCIPLINE, DIFFERENT TOOLS             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  CODE QUALITY:                                                      │
│  Python:   pyproject.toml  →  Ruff format  →  Ruff check  →  CI     │
│  C++:      .clang-format   →  clang-format →  clang-tidy  →  CI     │
│                                                                     │
│  TESTING:                                                           │
│  Python:   pytest          →  assert x == y  →  @pytest.fixture     │
│  C++:      ctest           →  EXPECT_EQ(x,y) →  ::testing::Test     │
│                                                                     │
│  COVERAGE:                                                          │
│  Python:   pytest --cov    →  htmlcov/       →  Codecov             │
│  C++:      gcov/llvm-cov   →  genhtml        →  Codecov             │
│                                                                     │
│  The tools are different, but the workflow is identical:            │
│  1. Configure once in project root                                  │
│  2. Run automatically (pre-commit, CI)                              │
│  3. Consistent, high-quality, well-tested code                      │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

What you learned in Python transfers directly to C++. The same mental model—configure tools, enforce automatically, test thoroughly, measure coverage—applies in both ecosystems. You’re not learning something new; you’re applying familiar concepts with different syntax.


10. Summary

In this lecture, you learned how to apply the same quality discipline to C++ that you already use for Python:

10.1 What You’ve Learned

Code Formatting:

Static Analysis:

Unit Testing:

Coverage:

Key Insight:

The tools are different, but the discipline is identical:
Configure → Enforce → Test → Measure → Repeat

11. Reflection Questions

Before moving on to Part 3, consider these questions:

  1. Configuration: How would you set up clang-format to match your team’s existing code style?

  2. Warnings as Errors: What are the pros and cons of using -Werror in production vs. development builds?

  3. Test Design: When would you use EXPECT_* vs. ASSERT_* in Google Test? Give an example of each.

  4. Coverage Gaps: If a function has 100% line coverage but still has a bug, what might be missing? How does branch coverage help?

  5. Tool Comparison: You’re joining a new team that uses C++. How would you explain the purpose of clang-tidy to a Python developer who knows Ruff?


12. What’s Next

Part 3: Hands-On Implementation will show you how to combine Python and C++ in a single project:

By the end of Part 3, your Road Profile Viewer will have C++ implementations that:

┌─────────────────────────────────────────────────────────────────────┐
│                         WHAT'S COMING IN PART 3                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Python Code                                      C++ Code          │
│  ┌────────────────────┐                    ┌────────────────────┐   │
│  │ calculate_ray_line │                    │ calculate_ray_line │   │
│  │ find_intersection  │ ──  pybind11 ──>   │ find_intersection  │   │
│  │ (easy to read)     │                    │ (fast to run)      │   │
│  └────────────────────┘                    └────────────────────┘   │
│                                                                     │
│  Your Python app calls C++ functions as if they were native Python  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk