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:
-
Source Code (
src/directory)- calculator.h, calculator.cpp
- string_processor.h, string_processor.cpp
-
Test Code (
tests/directory)- calculator_test.cpp
- string_processor_test.cpp
-
Build Files
- CMakeLists.txt
- README.md with build instructions
-
Test Report (
test_report.md)- Number of tests written
- Test results (all passing)
- Code coverage percentage (>80%)
- Screenshots of test execution
-
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)
- Mock Objects: Use Google Mock to test classes with dependencies
- Death Tests: Test that code crashes correctly for invalid inputs
- CI Integration: Set up GitHub Actions to run tests automatically
- 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 names ✅ Keep tests independent ✅ Run tests frequently ✅ Aim for high coverage but focus on quality
Resources
Good luck with your C++ testing journey! 🧪✨