← Back to Course
Lab 1

Lab 1 - C++ Unit Testing with Google Test

Lab Repository

yourusername/istqb-lab1-cpp-gtest

Quick Start

git clone https://github.com/yourusername/istqb-lab1-cpp-gtest.git

Lab 1: Introduction to C++ Unit Testing with Google Test

Objective

Learn to write unit tests for C++ code using Google Test framework. Practice test-driven development and achieve high code coverage.

Learning Outcomes

By completing this lab, you will:

  • ✅ Set up Google Test framework
  • ✅ Write unit tests for C++ functions and classes
  • ✅ Use assertions and test fixtures
  • ✅ Measure code coverage
  • ✅ Practice TDD methodology

Prerequisites

  • C++ compiler (g++ or clang++)
  • CMake 3.14 or higher
  • Git
  • Basic C++ knowledge

Estimated Time

⏱️ 3-4 hours


Part 1: Environment Setup (45 minutes)

Step 1: Install Required Tools

On Ubuntu/Debian:

sudo apt-get update
sudo apt-get install build-essential cmake git

On macOS:

brew install cmake

On Windows:

  • Install Visual Studio with C++ tools
  • Install CMake from cmake.org

Step 2: Create Project Structure

mkdir istqb-cpp-lab1
cd istqb-cpp-lab1

# Create directory structure
mkdir -p src tests build

Step 3: Set Up Google Test

Create CMakeLists.txt:

cmake_minimum_required(VERSION 3.14)
project(ISTQB_Lab1)

# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Download Google Test
include(FetchContent)
FetchContent_Declare(
  googletest
  URL https://github.com/google/googletest/archive/release-1.12.1.zip
)
FetchContent_MakeAvailable(googletest)

# Enable testing
enable_testing()

# Add source files
add_library(calculator src/calculator.cpp)
target_include_directories(calculator PUBLIC src)

# Add test executable
add_executable(calculator_tests tests/calculator_test.cpp)
target_link_libraries(calculator_tests calculator gtest_main)

# Discover tests
include(GoogleTest)
gtest_discover_tests(calculator_tests)

Part 2: Writing Your First Tests (60 minutes)

Task 2.1: Create Calculator Class

Create src/calculator.h:

#ifndef CALCULATOR_H
#define CALCULATOR_H

class Calculator {
public:
    int add(int a, int b);
    int subtract(int a, int b);
    int multiply(int a, int b);
    double divide(int a, int b);
    int power(int base, int exponent);
};

#endif

Create src/calculator.cpp:

#include "calculator.h"
#include <stdexcept>
#include <cmath>

int Calculator::add(int a, int b) {
    return a + b;
}

int Calculator::subtract(int a, int b) {
    return a - b;
}

int Calculator::multiply(int a, int b) {
    return a * b;
}

double Calculator::divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("Division by zero");
    }
    return static_cast<double>(a) / b;
}

int Calculator::power(int base, int exponent) {
    if (exponent < 0) {
        throw std::invalid_argument("Negative exponent not supported");
    }
    return std::pow(base, exponent);
}

Task 2.2: Write Unit Tests

Create tests/calculator_test.cpp:

#include <gtest/gtest.h>
#include "calculator.h"

// Test fixture for Calculator tests
class CalculatorTest : public ::testing::Test {
protected:
    Calculator calc;
};

// Test addition
TEST_F(CalculatorTest, AddPositiveNumbers) {
    EXPECT_EQ(calc.add(2, 3), 5);
}

TEST_F(CalculatorTest, AddNegativeNumbers) {
    EXPECT_EQ(calc.add(-2, -3), -5);
}

TEST_F(CalculatorTest, AddMixedNumbers) {
    EXPECT_EQ(calc.add(-5, 10), 5);
}

// Test subtraction
TEST_F(CalculatorTest, SubtractPositiveNumbers) {
    EXPECT_EQ(calc.subtract(10, 3), 7);
}

TEST_F(CalculatorTest, SubtractNegativeNumbers) {
    EXPECT_EQ(calc.subtract(-5, -3), -2);
}

// Test multiplication
TEST_F(CalculatorTest, MultiplyPositiveNumbers) {
    EXPECT_EQ(calc.multiply(4, 5), 20);
}

TEST_F(CalculatorTest, MultiplyByZero) {
    EXPECT_EQ(calc.multiply(10, 0), 0);
}

TEST_F(CalculatorTest, MultiplyNegativeNumbers) {
    EXPECT_EQ(calc.multiply(-3, -4), 12);
}

// Test division
TEST_F(CalculatorTest, DividePositiveNumbers) {
    EXPECT_DOUBLE_EQ(calc.divide(10, 2), 5.0);
}

TEST_F(CalculatorTest, DivideByZeroThrowsException) {
    EXPECT_THROW(calc.divide(10, 0), std::invalid_argument);
}

TEST_F(CalculatorTest, DivideResultsInDecimal) {
    EXPECT_NEAR(calc.divide(7, 2), 3.5, 0.001);
}

// Test power
TEST_F(CalculatorTest, PowerPositiveExponent) {
    EXPECT_EQ(calc.power(2, 3), 8);
}

TEST_F(CalculatorTest, PowerZeroExponent) {
    EXPECT_EQ(calc.power(5, 0), 1);
}

TEST_F(CalculatorTest, PowerNegativeExponentThrows) {
    EXPECT_THROW(calc.power(2, -1), std::invalid_argument);
}

Part 3: Building and Running Tests (30 minutes)

Step 1: Build the Project

cd build
cmake ..
make

Step 2: Run Tests

./calculator_tests

Expected Output:

[==========] Running 14 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 14 tests from CalculatorTest
[ RUN      ] CalculatorTest.AddPositiveNumbers
[       OK ] CalculatorTest.AddPositiveNumbers (0 ms)
[ RUN      ] CalculatorTest.AddNegativeNumbers
[       OK ] CalculatorTest.AddNegativeNumbers (0 ms)
...
[----------] 14 tests from CalculatorTest (1 ms total)

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

Step 3: Run with Verbose Output

./calculator_tests --gtest_print_time=1

Part 4: Advanced Testing (60 minutes)

Task 4.1: Parameterized Tests

Add to tests/calculator_test.cpp:

// Parameterized test for addition
class AdditionTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};

TEST_P(AdditionTest, AddNumbers) {
    Calculator calc;
    auto [a, b, expected] = GetParam();
    EXPECT_EQ(calc.add(a, b), expected);
}

INSTANTIATE_TEST_SUITE_P(
    AdditionTestCases,
    AdditionTest,
    ::testing::Values(
        std::make_tuple(1, 1, 2),
        std::make_tuple(0, 0, 0),
        std::make_tuple(-1, 1, 0),
        std::make_tuple(100, 200, 300),
        std::make_tuple(-50, -50, -100)
    )
);

Task 4.2: Test a More Complex Class

Create src/string_processor.h:

#ifndef STRING_PROCESSOR_H
#define STRING_PROCESSOR_H

#include <string>
#include <vector>

class StringProcessor {
public:
    std::string toUpperCase(const std::string& str);
    std::string toLowerCase(const std::string& str);
    bool isPalindrome(const std::string& str);
    std::vector<std::string> split(const std::string& str, char delimiter);
    std::string reverse(const std::string& str);
};

#endif

Your Task: Implement StringProcessor and write comprehensive tests!


Part 5: Code Coverage (45 minutes)

Step 1: Install Coverage Tools

sudo apt-get install lcov

Step 2: Enable Coverage in CMake

Add to CMakeLists.txt:

# Add coverage flags
if(CMAKE_BUILD_TYPE MATCHES Debug)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()

Step 3: Generate Coverage Report

# Rebuild with coverage
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make

# Run tests
./calculator_tests

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

# Open report
xdg-open coverage_report/index.html

Deliverables

Submit the following:

  1. Source Code (src/ directory)

    • calculator.h, calculator.cpp
    • string_processor.h, string_processor.cpp
  2. Test Code (tests/ directory)

    • calculator_test.cpp
    • string_processor_test.cpp
  3. Build Files

    • CMakeLists.txt
    • README.md with build instructions
  4. Test Report (test_report.md)

    • Number of tests written
    • Test results (all passing)
    • Code coverage percentage (>80%)
    • Screenshots of test execution
  5. Reflection (300-500 words)

    • Challenges faced
    • What you learned about TDD
    • How testing improved code quality

Grading Rubric

Criteria Points Description
Test Coverage 30 >80% code coverage achieved
Test Quality 25 Well-designed, comprehensive tests
Code Quality 20 Clean, well-structured C++ code
Documentation 15 Clear README and comments
Reflection 10 Thoughtful analysis
Total 100

Bonus Challenges (+10 points each)

  1. Mock Objects: Use Google Mock to test classes with dependencies
  2. Death Tests: Test that code crashes correctly for invalid inputs
  3. CI Integration: Set up GitHub Actions to run tests automatically
  4. Benchmark Tests: Use Google Benchmark to measure performance

Common Google Test Assertions

// Equality
EXPECT_EQ(actual, expected);
ASSERT_EQ(actual, expected);  // Stops test on failure

// Floating point
EXPECT_DOUBLE_EQ(actual, expected);
EXPECT_NEAR(actual, expected, tolerance);

// Boolean
EXPECT_TRUE(condition);
EXPECT_FALSE(condition);

// Exceptions
EXPECT_THROW(statement, exception_type);
EXPECT_NO_THROW(statement);

// String
EXPECT_STREQ(str1, str2);
EXPECT_STRCASEEQ(str1, str2);  // Case insensitive

Tips for Success

Write tests first (TDD approach) ✅ Test edge cases (zero, negative, max values) ✅ Use descriptive test namesKeep tests independentRun tests frequentlyAim for high coverage but focus on quality


Resources


Good luck with your C++ testing journey! 🧪✨