Sergei Zobov | Blog Software and robotics engineer

Example of writing C++ tests in ROS (Kinetic)

2019-02-08

Untested Code is Broken Code

Half year ago I wrote my first ROS C++ code. Along with it, I’ve started to look into documentation to find how to test my code. Unfortunately, I’ve found several resources, but all of them didn’t provide a good example of what should I actually do to make my test work well with ROS. Plus I wanted my test to show debug output if I need it. Here is a small boilerplate that you can use to cover your code with tests and take the advantages that I’ve mentioned above. Also I’ll provide you an example of how you can run your tests.

Environment

My current environment:

Directory structure

Typical ROS package directory:

.
├── ...
├── CMakeLists.txt
├── package.xml
├── src
│   ├── source.cpp
└── test
    ├── source_test.cpp
    └── source_test.launch

CMakeList.txt

<build instructions for your package is here>
...
if (CATKIN_ENABLE_TESTING)
    find_package(GTest REQUIRED)
    find_package(rostest REQUIRED)

    add_rostest_gtest(source_test
        test/source_test.launch
        test/source_test.cpp
        src/source.cpp
    )
    add_dependencies(source_test
    )
    target_link_libraries(source_test
        ${catkin_LIBRARIES} ${GTEST_LIBRARIES}
    )
endif()

Launch file

test/source_test.launch

It’s very useful to write ROSCONSOLE_FROMAT to make log-messages in your code prettier. Also it’s better do not write long test and limit it with the argument time-limit.

<launch>
    <env name="ROSCONSOLE_FORMAT" value="[${severity}] [${time}] ${logger}: ${message}"/>
    <test test-name="test" pkg="package_name" type="source_test" time-limit="10.0">
    </test>
</launch>

If you want to divide tests in the launch file you can add gtest_filter to the arguments for the tag <test>:

<launch>
    <env name="ROSCONSOLE_FORMAT" value="[${severity}] [${time}] ${logger}: ${message}"/>
    <test test-name="test" pkg="package_name" type="source_test" time-limit="10.0"
          args="--gtest_filter=TargetTest.test_ok">
    </test>
</launch>

Test file

test/source_test.cpp

#include <ros/ros.h>
#include <gtest/gtest.h>


class TargetTest: public ::testing::Test
{
public:
    TargetTest(): spinner(0) {};
    ~TargetTest() {};

    ros::NodeHandle* node;
    ros::AsyncSpinner* spinner;

    void SetUp() override
    {
        ::testing::Test::SetUp();
        this->node = new ros::NodeHandle("~");
        this->spinner = new ros::AsyncSpinner(0);
        this->spinner->start();
    };

    void TearDown() override
    {
        ros::shutdown();
        delete this->spinner;
        delete this->node;
        ::testing::Test::TearDown();
    }
};

TEST_F(TargetTest, test_ok)
{
    ASSERT_TRUE(false);
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "node_name");
    if (ros::console::set_logger_level(ROSCONSOLE_DEFAULT_NAME, ros::console::levels::Debug))
    {
        ros::console::notifyLoggerLevelsChanged(); // To show debug output in the tests
    }
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Run tests

# Build package and run tests. But it will show output only for log-messages with ERROR level.
$ catkin_make && catkin_make run_tests_package_name_rostest_test_source_test.launch
    
# After your tests are built, you can run it with DEBUG logging level
$ rostest --text package_name source_test.launch

By default, catkin_make returns 0 even if one or more tests are failed. For developing it’s acceptable, but if we want to run tests on CI we can use this commands:

# In order to run test on CI (return error if any tests are failed)
$ catkin_make run_tests && catkin_test_results

Content