Learn about unit testing

It’s common practice when developing software to create unit tests. While it might appear unnecessary at the outset, tests provide significant benefits:

  • When adding new functionality, or porting to a new platform, the tests allow developers to feel confident in the process.
  • They also inspire confidence in users that the software is at the appropriate level of quality.
  • They offer an opportunity to catch regressions.
  • They demonstrate how to use the library in practice.
  • They create opportunities for those new to the project to easily check their patches, and verify that the introduction of the new code has not created unintended negative changes.

You will notice that setting up testing precedes library code development.

There are many unit testing frameworks available, and C++ is not short of them. See this wikipedia article . This particular Learning Path uses GoogleTest as the testing framework.

Set up GoogleTest

One method you can use is to rely on the operating system platform to provide GoogleTest, then ask developers to install it on each machine they use, but this is an unnecessary step.

As testing is a cornerstone of your Matrix library development, GoogleTest should be installed automatically as a dependency in the build tree of your project.

One great feature of GoogleTest is that it provides a seamless integration with CMake.

Adding external dependencies is easily done with CMake. This is done with a separate CMakeLists.txt file, placed in the external/ directory. This file covers all external dependencies. It will be used by the main CMakeLists.txt.

Create the file external/CMakeLists.txt with the following content:

    

        
        
            cmake_minimum_required(VERSION 3.6)

project(external LANGUAGES CXX)

# Get the functionality to configure, build and install external project
# from CMake module 'ExternalProject'.
include(ExternalProject)

# Use the same compiler, build type and instalation directory than those
# from our caller.
set(EXTERNAL_PROJECT_CMAKE_ARGS
      -DCMAKE_CXX_COMPILER:PATH=${CMAKE_CXX_COMPILER}
      -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
      -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_INSTALL_PREFIX})

# Add 'googletext' as an external project, that will be cloned with git,
# from the official googletest repository, at version v1.14.
# We ask for a shallow clone, which is a clone with only the revision
# we are interested in rather than googletest's full history ---
# this makes the clone much faster (less data traffic), and uses much
# less disk space ; furthermore, as we are not developping googletest
# but just merely using it, we don't need thre full history. It will be
# built and installed with our build configuration passed with CMAKE_ARGS.
ExternalProject_Add(googletest
    PREFIX "external"
    GIT_REPOSITORY "https://github.com/google/googletest"
    GIT_TAG "v1.14.0"
    GIT_SHALLOW TRUE
    CMAKE_ARGS ${EXTERNAL_PROJECT_CMAKE_ARGS}
)
        
    

You might notice a new CMake feature: variables. Variables start with the $ character and have a name inserted between curly braces. A CMake variable can be set by the CMake itself, or by the user, and they can be modified or used as they are.

In this case, the variable ${EXTERNAL_PROJECT_CMAKE_ARGS} is set with the options to pass to CMake for installing the external dependencies:

  • ${CMAKE_CXX_COMPILER}: the C++ compiler used by CMake
  • ${CMAKE_BUILD_TYPE}: the type of build used by CMake (Release, Debug, …)
  • ${CMAKE_INSTALL_PREFIX}: where CMake will install the project

The project now looks like this:

    

        
        
            Matrix/
├── CMakeLists.txt
├── build/
│   ...
├── external/
│   └── CMakeLists.txt
├── include/
│   └── Matrix/
│       └── Matrix.h
├── lib/
│   └── Matrix/
│       └── Matrix.cpp
└── src/
    ├── getVersion.cpp
    └── howdy.cpp
        
    

Next, you need to use the new CMakeLists.txt in the top level CMake file.

Add the following lines after the Matrix project declaration in the top-level CMakeLists.txt:

    

        
        
            # ===================================================================
# Download, configure, build and install locally our external dependencies.
# This is done once, at configuration time.
# -------------------------------------------------------------------

# Build CMake command line so that it will use the same CMake configuration than
# the one we have been invoked with (generator, compiler, build type, build directory)
set(EXTERNAL_PROJECT_CMAKE_ARGS
      -G ${CMAKE_GENERATOR}
      -DCMAKE_CXX_COMPILER:PATH=${CMAKE_CXX_COMPILER}
      -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_TYPE}
      -DCMAKE_INSTALL_PREFIX:PATH=${CMAKE_BINARY_DIR})

# Download and configure our external dependencies
execute_process(
  COMMAND ${CMAKE_COMMAND}
      -S ${CMAKE_SOURCE_DIR}/external
      -B ${CMAKE_BINARY_DIR}/external
      ${EXTERNAL_PROJECT_CMAKE_ARGS}
)

# Build our external dependencies.
execute_process(
  COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR}/external
)
# Install our external dependencies.
execute_process(
  COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR}/external
)

# Import googletest package information (library names, paths, dependencies...)
set(GTest_DIR "${CMAKE_BINARY_DIR}/lib/cmake/GTest"
    CACHE PATH "Path to the googletest package configuration files")
find_package(GTest REQUIRED
  CONFIG
  NO_DEFAULT_PATH
  NO_PACKAGE_ROOT_PATH
  NO_SYSTEM_ENVIRONMENT_PATH
)
        
    

The variable ${CMAKE_GENERATOR} is what CMake uses to perform the build, usually GNU Make or Ninja, but it can be an IDE specific project file as well.

The variable ${CMAKE_SOURCE_DIR} is the path to the top level directory of your project where the main CMakeLists.txt is located, and ${CMAKE_BINARY_DIR} is the build directory.

At configuration time, CMake downloads, builds and installs GoogleTest and makes it available to your project using find_package.

Now if you build the project, CMake notices it has been updated and will perform all necessary steps.

The output below shows that besides the executable, CMake has configured, built, and installed GoogleTest.

Copy and paste the commands to run the build yourself to see the output:

    

        
        cd build
ninja
__output__-- The CXX compiler identification is AppleClang 15.0.0.15000309
__output__-- Detecting CXX compiler ABI info
__output__-- Detecting CXX compiler ABI info - done
__output__-- Check for working CXX compiler: /opt/homebrew/opt/ccache/libexec/clang++ - skipped
__output__-- Detecting CXX compile features
__output__-- Detecting CXX compile features - done
__output__-- The CXX compiler identification is AppleClang 15.0.0.15000309
__output__-- Detecting CXX compiler ABI info
__output__-- Detecting CXX compiler ABI info - done
__output__-- Check for working CXX compiler: /opt/homebrew/opt/ccache/libexec/clang++ - skipped
__output__-- Detecting CXX compile features
__output__-- Detecting CXX compile features - done
__output__-- Configuring done (0.3s)
__output__-- Generating done (0.0s)
__output__-- Build files have been written to: .../chapter-2/build/external
__output__[1/8] Creating directories for 'googletest'
__output__[2/8] Performing download step (git clone) for 'googletest'
__output__Cloning into 'googletest'...
__output__HEAD is now at f8d7d77 Bump version to v1.14 in preparation for release
__output__[3/8] Performing update step for 'googletest'
__output__[4/8] No patch step for 'googletest'
__output__[5/8] Performing configure step for 'googletest'
__output__-- The C compiler identification is AppleClang 15.0.0.15000309
__output__-- The CXX compiler identification is AppleClang 15.0.0.15000309
__output__-- Detecting C compiler ABI info
__output__-- Detecting C compiler ABI info - done
__output__-- Check for working C compiler: /opt/homebrew/opt/ccache/libexec/cc - skipped
__output__-- Detecting C compile features
__output__-- Detecting C compile features - done
__output__-- Detecting CXX compiler ABI info
__output__-- Detecting CXX compiler ABI info - done
__output__-- Check for working CXX compiler: /opt/homebrew/opt/ccache/libexec/clang++ - skipped
__output__-- Detecting CXX compile features
__output__-- Detecting CXX compile features - done
__output__-- Found Python3: /opt/homebrew/Frameworks/Python.framework/Versions/3.12/bin/python3.12 (found version "3.12.2") found components: Interpreter
__output__-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
__output__-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
__output__-- Found Threads: TRUE
__output__-- Configuring done (1.4s)
__output__-- Generating done (0.0s)
__output__-- Build files have been written to: .../chapter-2/build/external/external/src/googletest-build
__output__[6/8] Performing build step for 'googletest'
__output__[1/8] Building CXX object googlemock/CMakeFiles/gmock.dir/src/gmock-all.cc.o
__output__[2/8] Building CXX object googlemock/CMakeFiles/gmock_main.dir/src/gmock_main.cc.o
__output__[3/8] Building CXX object googletest/CMakeFiles/gtest_main.dir/src/gtest_main.cc.o
__output__[4/8] Building CXX object googletest/CMakeFiles/gtest.dir/src/gtest-all.cc.o
__output__[5/8] Linking CXX static library lib/libgtest.a
__output__[6/8] Linking CXX static library lib/libgtest_main.a
__output__[7/8] Linking CXX static library lib/libgmock.a
__output__[8/8] Linking CXX static library lib/libgmock_main.a
__output__[7/8] Performing install step for 'googletest'
__output__[0/1] Install the project...
__output__-- Install configuration: "Debug"
__output__-- Installing: .../chapter-2/build/include
__output__-- Installing: .../chapter-2/build/include/gmock
__output__-- Installing: .../chapter-2/build/include/gmock/gmock-matchers.h
__output__...
__output__-- Installing: .../chapter-2/build/include/gmock/gmock.h
__output__-- Installing: .../chapter-2/build/include/gmock/gmock-actions.h
__output__-- Installing: .../chapter-2/build/lib/libgmock.a
__output__-- Installing: .../chapter-2/build/lib/libgmock_main.a
__output__-- Installing: .../chapter-2/build/lib/pkgconfig/gmock.pc
__output__-- Installing: .../chapter-2/build/lib/pkgconfig/gmock_main.pc
__output__-- Installing: .../chapter-2/build/lib/cmake/GTest/GTestTargets.cmake
__output__-- Installing: .../chapter-2/build/lib/cmake/GTest/GTestTargets-debug.cmake
__output__-- Installing: .../chapter-2/build/lib/cmake/GTest/GTestConfigVersion.cmake
__output__-- Installing: .../chapter-2/build/lib/cmake/GTest/GTestConfig.cmake
__output__-- Up-to-date: .../chapter-2/build/include
__output__-- Installing: .../chapter-2/build/include/gtest
__output__-- Installing: .../chapter-2/build/include/gtest/gtest-matchers.h
__output__...
__output__-- Installing: .../chapter-2/build/include/gtest/gtest.h
__output__-- Installing: .../chapter-2/build/include/gtest/gtest-printers.h
__output__-- Installing: .../chapter-2/build/lib/libgtest.a
__output__-- Installing: .../chapter-2/build/lib/libgtest_main.a
__output__-- Installing: .../chapter-2/build/lib/pkgconfig/gtest.pc
__output__-- Installing: .../chapter-2/build/lib/pkgconfig/gtest_main.pc
__output__[8/8] Completed 'googletest'
__output__-- Install configuration: "Debug"
__output__-- Performing Test CMAKE_HAVE_LIBC_PTHREAD
__output__-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success
__output__-- Found Threads: TRUE
__output__-- Configuring done (5.9s)
__output__-- Generating done (0.0s)
__output__-- Build files have been written to: .../chapter-2/build
__output__[6/6] Linking CXX executable matrix-getVersion

        
    

Add your first test

Now that GoogleTest is available, you can add the first test.

In order to keep the project clean, all tests go inside a tests/ directory. One file, tests/main.cpp, contains the top level directions for testing.

As a project might contain many tests, it’s good to split them across several files inside the tests/ directory.

Create the top-level test in tests/main.cpp and paste the following code into the file:

    

        
        
            #include "gtest/gtest.h"

using namespace testing;

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

Create tests/Version.cpp and add the getVersion unit test into the file:

    

        
        
            #include "Matrix/Matrix.h"

#include "gtest/gtest.h"

using namespace MatComp;

TEST(Matrix, getVersion) {
    const Version &version = getVersion();
    EXPECT_EQ(version.major, 0);
    EXPECT_EQ(version.minor, 1);
    EXPECT_EQ(version.patch, 0);
}
        
    

This test invokes getVersion and checks that the major, minor and patch levels match the expected values.

The last step is to tell CMake about the tests. All tests are linked together in a single matrix-test executable (linking with the Matrix library and GoogleTest), and you add a convenience check target so the tests can be run easily.

Add the following at the bottom of the top-level CMakeLists.txt:

    

        
        
            # ===================================================================
# Testing
# -------------------------------------------------------------------
add_executable(matrix-test tests/main.cpp tests/Version.cpp)
target_link_libraries(matrix-test GTest::gtest Matrix)
add_custom_target(check
   COMMAND matrix-test --gtest_color=yes --gtest_output=xml:matrix-test.xml
)
        
    

Run the build again:

    

        
        cd build
ninja
__output__...
__output__[6/6] Completed 'googletest'
__output__-- Install configuration: "Debug"
__output__-- Configuring done (1.2s)
__output__-- Generating done (0.0s)
__output__-- Build files have been written to: .../chapter-2/build
__output__[3/3] Linking CXX executable matrix-test

        
    

And run the tests:

    

        
        ninja check
__output__...
__output__[==========] Running 1 test from 1 test suite.
__output__[----------] Global test environment set-up.
__output__[----------] 1 test from Matrix
__output__[ RUN      ] Matrix.getVersion
__output__[       OK ] Matrix.getVersion (0 ms)
__output__[----------] 1 test from Matrix (0 ms total)
__output__
__output__[----------] Global test environment tear-down
__output__[==========] 1 test from 1 test suite ran. (0 ms total)
__output__[  PASSED  ] 1 test.

        
    

Congratulations, your first unit test of the Matrix library passes!

What have you achieved so far?

Your directory structure now looks like this:

    

        
        
            Matrix/
├── CMakeLists.txt
├── build/
│   ├── howdy*              <- The howdy executable program
...
│   ├── libMatrix.a         <- The Matrix library
│   ├── matrix-getVersion*  <- The getVersion executable program
│   ├── matrix-test*        <- The Matrix library tests executable program
│   └── matrix-test.xml     <- The Matrix test results in XML format>
├── external/
│   └── CMakeLists.txt
├── include/
│   └── Matrix/
│       └── Matrix.h
├── lib/
│   └── Matrix/
│       └── Matrix.cpp
├── src/
│   ├── getVersion.cpp
│   └── howdy.cpp
└── tests/
    ├── Version.cpp
    └── main.cpp
        
    

CMake makes it easy to use GoogleTest as an external project. Adding unit tests as you go is now easy.

You have created the unit testing environment for your Matrix library and added a test. The infrastructure is now in place to implement the core of the Matrix processing library.

Back
Next