Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

This tutorial focuses on creating a simple C++ Calculator project using the divide() method function and testing it using the GoogleTest platform, and . This guide shows a complete way to do itthis, starting by writing tests for that function and then moving on to writing the source for the function. As you write your source code, you should run tests from time to time to check that the code is correct.

Table of Contents
maxLevel3
exclude.*:

Info
titleThe source code can be find here:

https://gitlab.eufus.psnc.pl/ach/ach-tutorials

...

Create The Calculator project

...

 Project preparation

To present an example of using GoogleTest, we first need to create a project in C++.  In the project directory named Calculator, before starting to write codes, it is necessary to prepare its structure. It will need two subdirectories named src and tst - one for the source codes and one for the tests codes. At this point, these folders can be filled with empty files with appropriate names for sources: operation.cpp; operations.h, and for tests: run_tests.cpp; test_operations.cpp. However, each of these files can be created later during development.

 Tests

 Tests supervisor file

To start writing tests, it is needed to create run_tests.cpp and test_operations.cpp (if not done previously). In order to force GoogleTest to automatically search for test files, the file run_tests.cpp comes in handy. Typically, a code inside this file will be universal for all kinds of projects:

Code Block
languagecpp
titlerun_tests.cpp
linenumberstrue
#include "gtest/gtest.h"

int main(int argc, char **argv){
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

 Tests suites file

However, the actual test cases are placed in a separate file, the name of which corresponds to the name of the file that stores the code to be tested. It is good practice to name the test file using the name of the source file being tested with the keyword test appended as a prefix or suffix separated by the underscore "_" or the dash "-". The goal of this tutorial is to test the division function stored in operations.cpp, although it does not exist at this point, it is predicted that it will be. Therefore, the file with the test code is named test_operations.cpp, and should look like this:

Code Block
languagecpp
titletest_operations.cpp
linenumberstrue
#include <gtest/gtest.h>
#include "operations.h"

TEST(DivideOperation, PositiveInput) {
    // Integer arguments
    ASSERT_EQ(divide(10, 5), 2);
    ASSERT_FLOAT_EQ(divide(5, 10), 0.5);
    // Floating-point arguments
    ASSERT_FLOAT_EQ(divide(10.0f, 5.0f), 2.0f);
    ASSERT_FLOAT_EQ(divide(5.0f, 10.0f), 0.5f);
}

TEST(DivideOperation, NegitiveInput) {
    // Integer arguments
    ASSERT_EQ(divide(-10, -5), 2);
    ASSERT_FLOAT_EQ(divide(-5, -10), 0.5);
    // Floating-point arguments
    ASSERT_FLOAT_EQ(divide(-10.0f, -5.0f), 2.0f);
    ASSERT_FLOAT_EQ(divide(-5.0f, -10.0f), 0.5f);
}

TEST(DivideOperation, ZerioInput) {
    // Integer arguments
    EXPECT_THROW(divide(10, 0), std::overflow_error);
    // Floating-point arguments
    EXPECT_THROW(divide(10.0f, 0.0f), std::overflow_error);
}

On top of the file, apart from including <gtest/gtest.h> , the GoogleTest framework, the operations.h header file with the test function declaration should be included. Then, test suites can be written for each condition given. The example above specifies three different conditions: a positive input argument, a negative input argument, and a zero input argument. There is also duplication of assertions for two types of arguments - floating point numbers and integers. This is especially important in the zero-input test suite, where a floating point number given as (0,0) may be evaluated as close to zero but not equal to it. However, in both cases this is done to pass a number of zero and should be detected by the function. In this case, it is expected to catch an overflow_error exception.

Sources

When the tests are ready, the source code can be developed. Following the above guidelines, two source files will be created in the src directory (if not already done), the operations.h and the operations.cpp. The first one will of course store the divide() function declaration, and the second one the definition. However Calculator, we will create the C++ source and header files in the appropriate directories. For the tutorial, we create operations.cpp in the src directory, which contains a simple divide function and the operation.h header file. However, C++ is a statically typed language and therefore we need to supply multiple definitions of a division function according to the type of its input arguments. The output of a function should always be floating point, but for the purposes of this tutorial, we predict that the input arguments can be both integers and floating point numbers. Therefore, two definitions have been introduced for each of these types.  The The header file should look like this:

...

The presented function has a mechanism for verifying whether the divisor (denominator) is equal to zero or is close to zero in the case of a floating-point number. For the purposes of this example, it has been arbitrarily assumed that numbers less than 1e-5 are zero, although this is not a generally accepted rule. In case the condition evaluates to True, an exception is thrown. To implement the exception, include the <stdexcept> library in the header file.  Under Under normal circumstances, the output value is computed and returned as floating point in both cases.

tests:

...

Our project folder should look like this:

Code Block
languagebash
titleCalculator/ $: tree
.
├── src
│   ├── operations.cpp
│   └── operations.h
└── tst
    ├── run_tests.cpp

...


    └── test_operations.cpp


Build the project  - CMake

 Build Configuration

 Root CMakeLists file

After the source and test code are ready, the project can be built and compiled. At the top of the project's folder structure, add CMakeLists.txt. The contents of the file are listed below. We want GoogleTest to search for test files automatically, and we do this thanks to the code stored in the run_tests.cpp source file. Typically, this code will be universal for all kinds of projects:

Code Block
title
languagecpprun_tests.cpp
linenumberstrue
#include "gtest/gtest.h"

int main(int argc, char **argv){
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}
cmake_minimum_required(VERSION 3.16)
project(Calculator)

set(CMAKE_CXX_STANDARD 11)

include_directories(src)
add_subdirectory(src)

enable_testing()
add_subdirectory(tst)

This is a simple setup for CMake builds, however it's important to get it right. At the beginning there are very basic lines that tell you the minimum CMake version required, the project name and the C++ standard used. Then the src directory is included and added. Next comes a very important line that enables testing and finally adds the tst directory as a subdirectory.

 Src CMakeList file

Then, in each subdirectory, another CMakeLists.txt file must be added accordingly. At first the ./src/CMakeLists.txt:

Code Block
languagecpp
linenumberstrue
add_library(src operations.cpp operations.h)

target_include_directories(src
        PUBLIC
        .
        )  

 Tst CMakeList file

Last but not least, the ./tst/CMakeLists.txt file which is a bit more elaborate and needs some explanationHowever, the actual test cases are placed in a separate file, the name of which corresponds to the name of the file that stores the code to be tested. It is good practice to name the test file using the name of the source file being tested with the keyword test appended as a prefix or suffix separated by the underscore "_" or the dash "-". The goal of this tutorial is to test the divide function stored in the operations.cpp file. Therefore, the file with the test code is named test_operations.cpp, and should look like this:

Code Block
languagecpp
titlerun_tests.cpp
linenumberstrue
#include <gtest/gtest.h>
#include "operations.h"

TEST(DivideOperation, PositiveInput) {
    // Integer arguments
    ASSERT_EQ(divide(10, 5), 2);
    ASSERT_FLOAT_EQ(divide(5, 10), 0.5);
    // Floating-point arguments
    ASSERT_FLOAT_EQ(divide(10.0f, 5.0f), 2.0f);
    ASSERT_FLOAT_EQ(divide(5.0f, 10.0f), 0.5f);
}

TEST(DivideOperation, NegitiveInput) {
    // Integer arguments
    ASSERT_EQ(divide(-10, -5), 2);
    ASSERT_FLOAT_EQ(divide(-5, -10), 0.5);
    // Floating-point arguments
    ASSERT_FLOAT_EQ(divide(-10.0f, -5.0f), 2.0f);
    ASSERT_FLOAT_EQ(divide(-5.0f, -10.0f), 0.5f);
}

TEST(DivideOperation, ZerioInput) {
    // Integer arguments
    EXPECT_THROW(divide(10, 0), std::overflow_error);
    // Floating-point arguments
    EXPECT_THROW(divide(10.0f, 0.0f), std::overflow_error);
}

...

include(FetchContent)
FetchContent_Declare(
        googletest
        URL https://github.com/google/googletest/archive/4ec4cd23f486bf70efcc5d2caa40f24368f752e3.zip
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

add_executable(
        test_operations
        run_tests.cpp
        test_operations.cpp
)

target_link_libraries(test_operations
        PRIVATE
        src
        gtest_main
        )

# Auto-discovery of the tests
include(GoogleTest)
gtest_discover_tests(test_operations
        PROPERTIES
        LABELS "unit"
        DISCOVERY_TIMEOUT
        240)

Most importantly, it is the FetchContent function that downloads the GoogleTesting framework when building the project. It is recommended that you use the exact same URL listed above for the purpose of this tutorial, however in future use this URL may be changed to a different version available in the googletest repository.

 Build final structure

Therefore, the final structure of the Calculator project should look like this:

Code Block
languagebash
titleCalculator/ $: tree
linenumberstrue
.
├── CMakeLists.txt
├── src
│   ├── CMakeLists.txt
│   ├── operations.cpp
│   └── operations.h
└── tst
    ├── CMakeLists.txt
    ├── run_tests.cpp
    └── test_operations.cpp

 Building project

A project prepared in this way can be built using the cmake command. This is done in 3 steps, the first is to create a directory to store the build, e.g. named built inside the project folder. Then, inside this directory, a project must be configured and a native build system generated. Finally, the project can be built, compiled and tested. It is shown below:

Code Block
languagebash
linenumberstrue
../TDD-cpp/Calculator/ $: mkdir -p ./built/
../TDD-cpp/Calculator/ $: cmake -S . -B ./built/
../TDD-cpp/Calculator/ $: cmake --build ./built/

 Running tests

Then the test executable can be found in the build directory, respectively built/tst/ named test_operations, as specified in CMakeLists.txt, and run:

Code Block
languagebash
linenumberstrue
../TDD-cpp/Calculator/ $: ./built/tst/test_operations

[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from DivideOperation
[ RUN      ] DivideOperation.PositiveInput
[       OK ] DivideOperation.PositiveInput (0 ms)
[ RUN      ] DivideOperation.NegitiveInput
[       OK ] DivideOperation.NegitiveInput (0 ms)
[ RUN      ] DivideOperation.ZerioInput
[       OK ] DivideOperation.ZerioInput (0 ms)
[----------] 3 tests from DivideOperation (0 ms total)

[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 3 tests.

A properly configured, built and run project should return the result as above with information about the tests performed and their results. In this case, all test suites should pass.