It’s common practice when developing software to create unit tests. While it might appear unnecessary at the outset, tests provide significant benefits:
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.
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 projectThe 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
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!
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.