Applied Software Design

6 minute read

Published:

Code: CMake and Catch2

Managing projects without CMake using multiple header and source files; Building projects with CMake and Make; CMakeLists.txt; Unit Testing using Catch2; Writing Test Cases

Step 1: What is the output

#include <iostream>
#include <string>

std::string GenerateWelcomeMessage(const std::string userName){
    std::string greeting = CreateGreeting(userName);
    greeting += " Welcome to the class!";
    return greeting;
}

std::string CreateGreeting(const std::string userName){
    return "Hello, " + userName + "!";
}

int main() {
    std::string userName = "ECE 3574";
    std::cout << GenerateWelcomeMessage(userName) << "\n";
}

Solution 1

#include <iostream>
#include <string>

std::string CreateGreeting(const std::string userName){
    return "Hello, " + userName + "!";
}

std::string GenerateWelcomeMessage(const std::string userName){
    std::string greeting = CreateGreeting(userName);
    greeting += " Welcome to the class!";
    return greeting;
}

int main() {
    std::string userName = "ECE 3574";
    std::cout << GenerateWelcomeMessage(userName) << "\n";
}

Solution 2

#include <iostream>
#include <string>

std::string GenerateWelcomeMessage(const std::string userName);
std::string CreateGreeting(const std::string userName);

std::string GenerateWelcomeMessage(const std::string userName){
    std::string greeting = CreateGreeting(userName);
    greeting += " Welcome to the class!";
    return greeting;
}
std::string CreateGreeting(const std::string userName){
    return "Hello, " + userName + "!";
}

int main() {
    std::string userName = "ECE 3574";
    std::cout << GenerateWelcomeMessage(userName) << "\n";
}

Step 2

// greeting.h
#ifndef GREETING_H
#define GREETING_H

#include <string>

std::string GenerateWelcomeMessage(const std::string userName);
std::string CreateGreeting(const std::string userName);

#endif


//greeting.cpp
#include "greeting.h"
#include <iostream>

std::string GenerateWelcomeMessage(const std::string userName){
    std::string greeting = CreateGreeting(userName);
    greeting += " Welcome to the class!";
    return greeting;
}

std::string CreateGreeting(const std::string userName){
    return "Hello, " + userName + "!";
}

//main.cpp
#include <iostream>
#include <string>
#include "greeting.h"

int main() {
    std::string userName = "ECE 3574";
    std::cout << GenerateWelcomeMessage(userName) << "\n";
    return 0;
}
g++ -std=c++20 main.cpp greeting.cpp && ./a.out

Step 3

g++ -std=c++20 -c *.cpp #compile all without linking
#greeting.o and main.o

g++ *.o #create executable file
#a.out

./a.out # execute
clear && g++ -std=c++20 -c *.cpp && g++ *.o && ./a.out
#Need to compile all *.cpp files
clear && g++ -std=c++20 -c main.cpp && g++ *.o && ./a.out
#However, manually tracking files which have been changed is very difficult task.

Make and Make

  • >greeting
    • >include
      • greeting.h
    • >src
      • greeting.cpp
      • main.cpp
    • CMakeLists.txt

CMakeLists.txt

cmake_minimum_required(VERSION 3.5.1)

# Set the C++ standard to C++23
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# used internally by CMake to identify your project
project(greeting)

# Include the directory headers are located
include_directories(${CMAKE_SOURCE_DIR}/include)

# Add the main executable
add_executable(greeting src/main.cpp src/greeting.cpp)

Mac

mkdir build
cd build
cmake ..
make
./greeting

Windows

mkdir build 
cd build
cmake .. -G "MinGW Makefiles"
cmake --build .
greeting.exe

Catch2

  • >greeting
    • >include: greeting.h
    • >src: greeting.cpp; main.cpp
    • >build
    • >tests
      • >catch2
        • catch.hpp
      • test.cpp
    • CMakeLists.txt

CMakeLists.txt

cmake_minimum_required(VERSION 3.5.1)

# Set the C++ standard to C++23
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# used internally by CMake to identify your project
project(greeting)

# Include the directory headers are located
include_directories(${CMAKE_SOURCE_DIR}/include)

# Add the main executable
add_executable(greeting src/main.cpp src/greeting.cpp)

####################################################

# Add the test executable
add_executable(my_test src/greeting.cpp tests/test.cpp)

# Include directories for the test target
target_include_directories(my_test PRIVATE ${PROJECT_SOURCE_DIR}/include)

# Since Catch2 is header-only, we don't need to link a library
# No need to link Catch2 as it's just a header file

# Enable testing
enable_testing()

# Register the test executable with CTest
add_test(NAME my_test COMMAND my_test)
// tests/test.cpp
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
#include "greeting.h"  

TEST_CASE("Testing CreateGreeting", "[CreateGreeting]") {
    REQUIRE(CreateGreeting("Alice") == "Hello, Alice!");
    REQUIRE(CreateGreeting("") == "Hello, !");
    REQUIRE(CreateGreeting("1") == "Hello, 1!");
}

TEST_CASE("Testing GenerateWelcomeMessage", "[GenerateWelcomeMessage]") {
    REQUIRE(GenerateWelcomeMessage("Alice") == "Hello, Alice! Welcome to the class!");
    REQUIRE(GenerateWelcomeMessage("") == "Hello, ! Welcome to the class!");
    REQUIRE(GenerateWelcomeMessage("1") == "Hello, 1! Welcome to the class!");
}
// src/greeting.cpp
#include "greeting.h"
#include <iostream>

std::string GenerateWelcomeMessage(const std::string userName){
    if (userName == "") {
        throw std::invalid_argument("name cannot be empty");
    }
    std::string greeting = CreateGreeting(userName);
    greeting += " Welcome to the class!";
    return greeting;
}

std::string CreateGreeting(const std::string userName){
    if (userName == "") {
        throw std::invalid_argument("name cannot be empty");
    }
    return "Hello, " + userName + "!";
}
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
#include "greeting.h"  

TEST_CASE("CreateGreeting with invalid input", "[CreateGreeting][invalidinput]") {

    SECTION("Empty input should throw exception") {
        REQUIRE_THROWS_AS(CreateGreeting(""), std::invalid_argument);
    }
}

TEST_CASE("GenerateWelcomeMessage with invalid input", "[GenerateWelcomeMessage][invalidinput]") {

    SECTION("Empty input should throw exception") {
        REQUIRE_THROWS_AS(GenerateWelcomeMessage(""), std::invalid_argument);
    }
}

TEST_CASE("CreateGreeting with valid inputs", "[CreateGreeting][validinput]") {

    SECTION("Greeting Alice") {
        REQUIRE(CreateGreeting("Alice") == "Hello, Alice!");
    }
    SECTION("Greeting numeric string") {
        REQUIRE(CreateGreeting("1") == "Hello, 1!");
    }
}

// Test case for GenerateWelcomeMessage with valid inputs
TEST_CASE("GenerateWelcomeMessage with valid inputs", "[GenerateWelcomeMessage][validinput]") {

    SECTION("Welcome Alice") {
        REQUIRE(GenerateWelcomeMessage("Alice") == "Hello, Alice! Welcome to the class!");
    }
    SECTION("Welcome numeric string") {
        REQUIRE(GenerateWelcomeMessage("1") == "Hello, 1! Welcome to the class!");
    }
}
cmake .. && make && ./my_test
./my_test "[CreateGreeting]" 
./my_test “[CreateGreeting][invalidinput]"
./my_test "[CreateGreeting][validinput]"
./my_test "[GenerateWelcomeMessage]"
./my_test "[GenerateWelcomeMessage][invalidinput]"
./my_test “[GenerateWelcomeMessage][validinput]"
./my_test "[validinput]"

Behaviour Driven Development (BDD) Style

#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
#include "greeting.h"  

SCENARIO("CreateGreeting handles empty input correctly", "[CreateGreeting][invalidinput]") {
    GIVEN("an invalid input") {
        std::string invalidInput = "";
        WHEN("the input is empty") {
            THEN("an exception should be thrown") {
                REQUIRE_THROWS_AS(CreateGreeting(invalidInput), std::invalid_argument);
            }
        }
    }
}
SCENARIO("CreateGreeting handles valid input correctly", "[CreateGreeting][validinput]") {
    GIVEN("a valid input") {

        WHEN("the input is 'Alice'") {
            std::string validInput = "Alice";
            THEN("the greeting should be 'Hello, Alice!'") {
                REQUIRE(CreateGreeting(validInput) == "Hello, Alice!");
            }
        }

        WHEN("the input is a numeric string '1'") {
            std::string numericInput = "1";
            THEN("the greeting should be 'Hello, 1!'") {
                REQUIRE(CreateGreeting(numericInput) == "Hello, 1!");
            }
        }
    }
}
SCENARIO("GenerateWelcomeMessage handles empty input correctly", "[GenerateWelcomeMessage][invalidinput]") {
    GIVEN("an invalid input") {
        std::string invalidInput = "";
        WHEN("the input is empty") {
            THEN("an exception should be thrown") {
                REQUIRE_THROWS_AS(GenerateWelcomeMessage(invalidInput), std::invalid_argument);
            }
        }
    }
}
SCENARIO("GenerateWelcomeMessage handles valid input correctly", "[GenerateWelcomeMessage][validinput]") {
    GIVEN("a valid input") {

        WHEN("the input is 'Alice'") {
            std::string validInput = "Alice";
            THEN("the welcome message should be 'Hello, Alice! Welcome to the class!'") {
                REQUIRE(GenerateWelcomeMessage(validInput) == "Hello, Alice! Welcome to the class!");
            }
        }

        WHEN("the input is a numeric string '1'") {
            std::string numericInput = "1";
            THEN("the welcome message should be 'Hello, 1! Welcome to the class!'") {
                REQUIRE(GenerateWelcomeMessage(numericInput) == "Hello, 1! Welcome to the class!");
            }
        }
    }
}
cmake .. && make && ./my_test
./my_test "[CreateGreeting]" 
./my_test “[CreateGreeting][invalidinput]"
./my_test "[CreateGreeting][validinput]"
./my_test "[GenerateWelcomeMessage]"
./my_test "[GenerateWelcomeMessage][invalidinput]"
./my_test “[GenerateWelcomeMessage][validinput]"
./my_test "[validinput]"