Problem: How to use gtest in a ROS program?

Gtest is very powerful for unit testing. I would like to use it in my ros program.

Unfortunately, the documentation on Ros.org talking about the gtest is far from helpful [1].

Suggestion from ros.answer [2] and the article written by Billy McCafferty [3] are more useful for me to understand how to use gtest in ros.

Here I would like to show the complete procedure to use gtest in ROS program through a minimum example.

Result of Running Gtest with ROS program

Generally, the article is organized as:

  1. Create a ros package using catkin
  2. Install gtest
  3. Use gtest in ros

1. Create a ROS package

  • create a catkin workspace for ros packages, name this workspace ros_gtest_example
mkdir -p ros_gtest_example/src

cd ros_gtest_example/src

#a .catkin_workspace file will be created
catkin_init_workspace
  • create a ros package in current workspace, name the package ros_gtest
cd src

catkin_create_pkg ros_gtest std_msg roscpp
  • in this package, create two simple nodes, rostalker and roslistener based on the tutorial

(1) create a talker node to publish a std_msg, the header and cpp files are:

#ifndef ROS_TALKER_H
#define ROS_TALKER_H

#include <ros/ros.h>
#include "std_msgs/String.h"
#include <sstream>

class RosTalker{
public:
    RosTalker(){
        msg_pub = nh.advertise<std_msgs::String>("talker_msg", 1000);
    }
    ros::NodeHandle nh;
    ros::Publisher msg_pub;
    void talk();
    int add(int, int);
};
#endif
#include "rostalker.h"

void RosTalker::talk(){
    std_msgs::String msg;
    std::stringstream ss;
    ss<<"hello world";
    msg.data = ss.str();
    msg_pub.publish(msg);
}

int RosTalker::add(int a, int b){
    return a + b;
}

int main(int argc, char **argv){
    ros::init(argc, argv, "rosTalker");
    RosTalker rt;
    ros::Rate loop_rate(5);
    while(ros::ok()){
        rt.talk();
        loop_rate.sleep();
    }
    return 0;
}

(2) create a listener node to subscribe the talker’s topic, the header and cpp files are:

#ifndef ROS_LISTENER_H
#define ROS_LISTENER_H
#include <ros/ros.h>
#include "std_msgs/String.h"

class RosListener{
public:
    RosListener(){
    msg_sub = nh.subscribe("talker_msg", 1000, &RosListener::msgCallback, this);
    }
    ros::NodeHandle nh;
    ros::Subscriber msg_sub;
    void msgCallback(const std_msgs::String & );
};
#endif
#include "roslistener.h"

void RosListener::msgCallback(const std_msgs::String & msg){
    ROS_INFO_STREAM("I hear from talker: "<< msg.data.c_str());
}

int main(int argc, char **argv){
    ros::init(argc, argv, "rosListener");
    RosListener rl;
    ros::spin();
    return 0;
}
  • edit file package.xml and CMakeLists.txt in current package folder
<?xml version="1.0"?>
<package>
  <name>ros_gtest</name>
  <version>0.0.0</version>
  <description>The ros_gtest package</description>
  <maintainer email="ysong@gmail.com">ysong</maintainer>
  <license>GPLv2</license>
  <buildtool_depend>catkin</buildtool_depend>
  <build_depend>roscpp</build_depend>
  <build_depend>std_msgs</build_depend>
  <run_depend>roscpp</run_depend>
  <run_depend>std_msgs</run_depend>
</package>
cmake_minimum_required(VERSION 2.8.3)
set(PROJECT_NAME ros_gtest)
project(${PROJECT_NAME})

find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
)

include_directories(
  include/${PROJECT_NAME}
  ${catkin_INCLUDE_DIRS}
)
add_executable(rostalker src/rostalker.cpp)
add_executable(roslistener src/roslistener.cpp)
target_link_libraries(roslistener
  ${catkin_LIBRARIES} pthread
)
target_link_libraries(rostalker
  ${catkin_LIBRARIES} pthread
)
  • compile the ros nodes in the folder ros_gtest_example
catkin_make
  • create a simple launch file in the directory of ros_gtest
<launch>
     <node name="rostalker1" pkg="ros_gtest" type="rostalker"/>   
     <node name="roslistener1" pkg="ros_gtest" type="roslistener" output="screen"/>
</launch>
  • so far the directory of ros_gtest_example is shown below:

We can test these two nodes by executing roslaunch

source devel/setup.bash

roslaunch ros_gtest twonodes.launch

2. Install gtest

On ubuntu, it is easy to install gtest by command:

sudo apt-get install libgtest-dev

cd /usr/src/gtest

sudo cmake CMakeLists.txt

sudo make

#copy or symlink libgtest.a and ligtest_main.a to /usr/lib folder
sudo cp *.a /usr/lib

To make sure the gtest is configured properly, I write a simple file called simple_test.cpp for testing purpose:

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

// bad function:
// for example: how to deal with overflow?
int add(int a, int b){
    return a + b;
}

TEST(NumberCmpTest, ShouldPass){
    ASSERT_EQ(3, add(1,2));
}

TEST(NumberCmpTest, ShouldFail){
    ASSERT_EQ(INT_MAX, add(INT_MAX, 1));
}

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

and also create a CMakeLists.txt in which invokes the gtest library:

cmake_minimum_required(VERSION 2.8)

# Locate GTest
find_package(GTest REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})
 
# Link runTests with what we want to test and the GTest and pthread library
add_executable(simple_test simple_test.cpp)
target_link_libraries(simple_test ${GTEST_LIBRARIES} pthread)

Compile with cmake && make, the executable should give result:

3. Use gtest in ros

Now, we start to integrate gtest with above-mentioned ros program. There are two tricky steps to do this. First one is about the CMake file, the second one is write test file to test functions for the ros nodes.

Not to bother with the second step, I just create a folder called test in the ros_gtest directory. In this folder, cp above file simple_test.cpp and renamed it test_talker.cpp:

cp <path-to-simple_test>/simple_test.cpp <path-to-catkin-ws>/src/ros_gtest/test/

(1) Add gtest in the CMake file so we can use catkin to compile:

cmake_minimum_required(VERSION 2.8.3)
set(PROJECT_NAME ros_gtest)
project(${PROJECT_NAME})

find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
)
#NEW:Locate GTest
find_package(GTest REQUIRED)
include_directories(
  include/${PROJECT_NAME}
  ${catkin_INCLUDE_DIRS}
  ${GTEST_INCLUDE_DIRS} #NEW: GTest Lib
)
add_executable(rostalker src/rostalker.cpp)
add_executable(roslistener src/roslistener.cpp)
target_link_libraries(roslistener
  ${catkin_LIBRARIES} pthread
)
target_link_libraries(rostalker
  ${catkin_LIBRARIES} pthread
)
#NEW: GTest Node
catkin_add_gtest(talker-test test/test_talker.cpp)
target_link_libraries(talker-test ${catkin_LIBRARIES})

Now we should be able to compile and execute the test file using catkin:

catkin_make tests

#the test file is also a ros node
roscore && rosrun ros_gtest talker-test

(2) Modifiy test files, ros node file and cmake file:

The ultimate objective is to test functions for ros nodes.

Suppose I want to test the member function of RosTalker class, I have to include the rostalker in the test file. So far, the solution I have is to comment the main function in the rostalker node, instead, make publish or ros::spin in the member function. I got this idea from the article [3], in which the author used the same trick for the message endpoint class.

(2.1) Modify rostalker.cpp by removing main function:

#include "rostalker.h"

void RosTalker::talk(){
    std_msgs::String msg;
    std::stringstream ss;
    ss<<"hello world";
    msg.data = ss.str();
    msg_pub.publish(msg);
    ros::spinOnce(); // optional
}

int RosTalker::add(int a, int b){
    return a + b;
}

(2.2) Modify test_talker.cpp by calling rostalker’s member function:

#include <gtest/gtest.h>
#include <climits>
#include "rostalker.h"
// bad function:
// for example: how to deal with overflow?
int add(int a, int b){
    return a + b;
}

TEST(NumberCmpTest, ShouldPass){
    ASSERT_EQ(3, add(1,2));
}

TEST(NumberCmpTest, ShouldFail){
    ASSERT_EQ(INT_MAX, add(INT_MAX, 1));
}

TEST(RtTest, TalkerFunction){
    RosTalker rt;
    ASSERT_EQ(3, rt.add(1,2));
}

int main(int argc, char **argv) {
    testing::InitGoogleTest(&argc, argv);
    // do not forget to init ros because this is also a node
    ros::init(argc, argv, "talker_tester");
    return RUN_ALL_TESTS();
}

(2.3) Modify CMakeLists.txt: no executable will be generated by roswalker class since no main function

cmake_minimum_required(VERSION 2.8.3)
set(PROJECT_NAME ros_gtest)
project(${PROJECT_NAME})

find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
)
#NEW:Locate GTest
find_package(GTest REQUIRED)
include_directories(
  include/${PROJECT_NAME}
  ${catkin_INCLUDE_DIRS}
  ${GTEST_INCLUDE_DIRS} #NEW: GTest Lib
)
#add_executable(rostalker src/rostalker.cpp)
#target_link_libraries(rostalker
#  ${catkin_LIBRARIES} pthread
#)
add_executable(roslistener src/roslistener.cpp)
target_link_libraries(roslistener
  ${catkin_LIBRARIES} pthread
)
#NEW: GTest Node
catkin_add_gtest(talker-test test/test_talker.cpp src/rostalker.cpp)
target_link_libraries(talker-test ${catkin_LIBRARIES})

Finally, we can run the gtest to test the member function of rostalker node:

catkin_make

catkin_make tests

#the test file is also a ros node
roscore && rosrun ros_gtest talker-test

The final result is shown in the top figure.

All source files can be found in my github, repo: ros_gtest_example.

References

  1. Ros - gtest
  2. rostest - Minimum Working Example
  3. Part V: Developing and Testing the ROS Message Endpoint

Yang Song

Ph.D. Student in Robotics