C++ - Writing GTest for ROS

Test! Test! Test!

Posted by Rico's Nerd Cluster on May 21, 2023

Concepts

Google Test (a.k.a GTest) is an open source unit testing structure. From the official website

  • Test Suite vs Test Case: A test suite contains one or many tests. You should group your tests into test suites that reflect the structure of the tested code. When multiple tests in a test suite need to share common objects and subroutines, you can put them into a test fixture class.

Some common macros include:

  • EXPECT_EQ vs ASSERT_EQ: EXPECT_* versions generate nonfatal failures, which don’t abort the current function. Usually EXPECT_* are preferred, as they allow more than one failure to be reported in a test. However, you should use ASSERT_* if it doesn’t make sense to continue when the assertion in question fails.

Usage

  • Install GTest if your environment doesn’t have it
1
2
3
4
5
6
git clone https://github.com/google/googletest
cd googletest
mkdir -p build install
cd build
cmake -DCMAKE_INSTALL_PREFIX=../install .. Install locally
make -j32 install

Simple Test Case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Test function
double getFrobeniusNorm(const cv::Mat &m){
    // element-wise matrix multiplication 
    // cv::sum sums of elements across all dimensions
    return std::sqrt(cv::sum(m.mul(m))[0]);
}

// Test case
#include <gtest/gtest.h>

TEST(CvUtilsTest, TestFrobeniusNorm) {
    cv::Mat_<double> m (3, 3);
    m << 1.0, 2.0, 3.0,
        0.0, 0.0, 0.0,
        1.0, 2.0, 0.0;
    double norm = getFrobeniusNorm(m);
    EXPECT_EQ(norm, std::sqrt(1 + 4 + 9 + 1 + 4));
}

Test Fixture

Test fixture is a mechanism to share code between test cases in a test suite. One common misconception is “the text fixture will be reused across tests”. THAT IS NOT TRUE! Test fixtures are only for sharing initialization code. Below, we have an example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyTestFixture : public ::testing::Test{
    protected:
        void SetUp() override {
           str_ = "Hello World";
           std::cout<<str_<<std::endl;
        }
        void TearDown() override {
            // For extra destruction work
        }

        static void SetUpTestSuite(){
            // Initialize shared resource (like database access) for the entire test suite
        }
        static void TearDownTestSuite(){
            // Destruct test suite's shared resources
        }
    std::string str_;
};

TEST_F(MyTestFixture, SomeTest){
}
  1. Note, to use a test fixture, one needs TEST_F instead of TEST.
  2. Another option is to use constructors. The official documentation has a good recommendation on when to use which.. The TL;DR is:

    • Use ctor when we want to initialize const member variables. If we know they wouldn’t change throughout the tests, it’d be a good practice to do so.
    • Use SetUp() when:
      • We need to call a virtual function during initialization. See this article for more about the rationale.
      • If a fatal failure could happen, ASSERT_* is a good choice for that purpose, but it cannot be used in ctor or dtor and can be only used in SetUp()
      • SetUp() can catch exceptions. Arguably, it could be a good idea if you don’t want uncaught exceptions to interrupt your normal tests (e.g., an emergency test fix)

Test Environment

Test environment helps initialize test resource that’s shared for the entire test binary, e.g., logging system initialization. One potential bug is this environment could be shared across multiple compilation units, if they comprise the same binary.

1
2
3
4
5
6
7
8
9
10
11
12
class Environment : public ::testing::Environment{
    public:
        ~Environment() override {}
        void SetUp() override {std::cout<<"This is intialized only once globally"<<std::endl;}
        void TearDown() override {std::cout<<"This is destructed only once globally"<<std::endl;}
};

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

General Gtest with CMake

If using CMakeLists.txt, we can create a test executable for each of them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Test files
file(GLOB_RECURSE TEST_FILES "tests/unit/*cpp")
foreach(TEST_FILE ${TEST_FILES})
    # NAME_WE means "name without extension"
    get_filename_component(TEST_EXEC_NAME ${TEST_FILE} NAME_WE)
    add_executable(${TEST_EXEC_NAME}_test ${TEST_FILE})
    add_test(NAME ${TEST_EXEC_NAME}_test COMMAND ${TEST_EXEC_NAME}_test)
    target_link_libraries(${TEST_EXEC_NAME}_test
        ${PROJECT_NAME}_dependencies
        gtest
        gtest_main
        Threads::Threads
    )
endforeach()
enable_testing()
  • add_test() does NOT create the test executable, it simply registers the exectuble as a test.
  • Threads::Threads must be added to the above gtest. Otherwise, the gtest will fail silently, which is really annoying!!)

Gtest For ROS

The Snippet in section “General Gtest with CMake” already can be built by catkin:

1
2
3
4
5
6
# build all executables
catkin build
# Run tests
catkin test
# Or for a specific package
catkin test <PACKAGE>
  • The test executable is in devel/lib/${PACKAGE_NAME}
  • Upon failures, catkin test could throw a failure “double freeing” if we do not have a main() in our tests and uses gtest_main. This wouldn’t arise if we run the executable alone. So, my best guess is this is a memory-freeing bug in catkin test with gtest_main. But if we add a main ourselves, and uses catkin_add_gtest(), we should be all good.
  • catkin test requires roscore to be running. If it’s not run, it will show as stuck in the building phase.
  • The CMakeLists.txt is as below:
1
2
3
4
5
6
7
8
9
10
file(GLOB_RECURSE TEST_FILES "tests/unit/*cpp")
foreach(TEST_FILE ${TEST_FILES})
    # NAME_WE means "name without extension"
    get_filename_component(TEST_EXEC_NAME ${TEST_FILE} NAME_WE)
    catkin_add_gtest(${TEST_EXEC_NAME}_test ${TEST_FILE})
    target_link_libraries(${TEST_EXEC_NAME}_test
        ${catkin_LIBRARIES}
    )
endforeach()
enable_testing()
  • test.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <ros/ros.h>
#include <gtest/gtest.h>

TEST(TestFeatureDetection, TestORB){
    EXPECT_EQ(1,2);
}


int main(int argc, char **argv){
    testing::InitGoogleTest(&argc, argv);
    ros::init(argc, argv, "tester");
    ros::NodeHandle nh;
    return RUN_ALL_TESTS();
}