ROS Resources: Documentation | Support | Discussion Forum | Index | Service Status | ros @ Robotics Stack Exchange
Ask Your Question
1

Ideal C++ Node Architecture for Unit Testing

asked 2020-02-17 08:29:06 -0500

cferone gravatar image

updated 2020-02-17 11:26:12 -0500

Hi,

I have been using a combination of gtest and rostest to validate (or verify?) my code. I was wondering if there was a better way to architect my code so that my unit tests do not unnecessarily require me to spool up a roscore. In other words, if I don't have to use rostest, I'd prefer to just use gtest.

For example, all of my C++ nodes follow the following the same basic structure structure:

.
+-- src 
|    +-- src/super_cool_stuff_node.cpp 
|    +-- src/super_cool_stuff.cpp
+-- includes 
|    +-- include/supper_cool_stuff.h

Where src/super_cool_stuff_node.cpp doesn't contain any real logic. I just declare the node handle, instantiate my custom class, and set the execution rate. In fact, I use this template for just about all C++ nodes I write.

#include "myPackage/supper_cool_stuff.h"

int main(int argc, char **argv)
{
  ros::init(argc, argv, "super_cool_node"); //node name
  ros::NodeHandle nh; // create a node handle;

  //Instantiate the class
  MyClass my_class(&nh);

  // Tell ROS how fast to run this node.
  ros::Rate loop_rate(10);

  while (ros::ok())
  {
    ros::spinOnce();
    my_class.doStuff();
    loop_rate.sleep();
  }

  return 0;
}

I then define and declare the class in src/super_cool_stuff.cpp and include/supper_cool_stuff.h, respectively.

#include "myPackage/supper_cool_stuff.h"
#define pi 3.14159265358979323846

MyClass::MyClass(ros::NodeHandle* nodehandle) :
nh_(*nodehandle)
{ // constructor
  ROS_INFO("Beginning of Class Constructor");

  node_name_ = ros::this_node::getName();

  //Define Static Calibrations
  nh_.param<std::string>("robot_name", robot_name_, "no_name");

  //Initialize Variables
  finished_placing_waypoints_ = false;

  //Define Subscribers
  odom_sub_ = nh_.subscribe(odom_topic_source_, 1, &MyClass::odomMsgCallback, this);

  //Define Publishers
  autonomy_commands_pub_ = nh_.advertise<geometry_msgs::TwistStamped>(node_name_ + "/cmd_vel", 10);

  //Dynamic Reconfigure
  dynamic_reconfigure::Server<MyClass::pfConfig>::CallbackType f; // Start up dynamic reconfigure server
  f = boost::bind(&MyClass::dynamicReconfigCallback, this, _1, _2);
  server_.setCallback(f);

  ROS_INFO("End of Class Constructor");
}

// Class Methods

float MyClass::doSomethingSpecific(float speed, bool status)
{

}

and finally the header file for the class:

#ifndef MY_CLASS_H
#define MY_CLASS_H

#include <ros/ros.h>
#include <geometry_msgs/Twist.h>

class MyClass
{

public:
  MyClass(ros::NodeHandle* nodehandle);

  ros::NodeHandle nh_;

  bool finished_placing_waypoints_;    
  ros::Subscriber odom_sub_;
  ros::Publisher autonomy_commands_pub_;
  dynamic_reconfigure::Server<path_follower::pfConfig> server_;

  float doSomethingSpecific(float speed, bool status);

}

The problem that I have is that if I want to unit test the class method doSomethingSpecific() using gtest, I have to instantiate the class MyClass. The class MyClass requires a node handle to be passed into it. I can't define a node handle without running roscore. I understand a node handle is required for subscribers, publishers, parameters, etc., but for the class methods that truly don't need a node handle, like doSomethingSpecific(), is there a better way to architect the node so that these functions can be tested without running roscore? Should these class methods be extracted into a second class? Should I move all of the logic that requires a node handle out of the class file src/super_cool_stuff.cpp and into src/super_cool_stuff_node.cpp? I'm trying to figure out the best way to architect the code for test driven development.

Thanks for your help.

edit retag flag offensive close merge delete

1 Answer

Sort by ยป oldest newest most voted
0

answered 2020-04-10 11:43:30 -0500

james1345 gravatar image

The only problem that can't be solved with another level of abstraction is too many levels of abstraction

I'm not promising it'll make your code any easier to read, but rather than have your "myclass" be given a NodeHandle in its constructor, you instead pass it abstract interfaces. You'd probably want two, a subscription and a publish object interface

These interfaces would provide the methods to do subscribing and publishing. E.g. the MyPublisher provides a publish method. But no implementation is given. When your "doSomethingSpecific" withes to publish, it would call the publisher.

You then implement each of publisher and subscriber twice: a ROS publisher, which takes the nodeHandle in its constructor (as you do) and publishes on the rostopic, and a mock version which simply discards the message (or stores it somewhere, or increments a counter, or whatever).

You have now decoupled your code from ROS completely. You can test the class using the mock version and gtest, and run the real one with ROS.

An (pseudo) example with the publisher would look something like:

class Publisher {

    virtual void publish(geometry_msgs::TwistStamped msg) = 0;
}

class MockPublisher : Publisher{
    void publish override (geometry_msgs::TwistStamped msg){
        // do nothing
    }
}

class RosPublisher : Publisher{
private:

    ros::Publisher p;

public:

    RosPublisher(ros::NodeHandle* nodehandle){
        p = nh->.advertise<geometry_msgs::TwistStamped>(node_name_ + "/cmd_vel", 10);
    }

    void publish override (geometry_msgs::TwistStamped msg){
        p.publish(msg)
    }
}


class MyClass {
    Publisher p_;

public:
    MyClass(Publisher p): p_(p) {}

    void doSomethingSpecific(float f0, bool b0){
        // cool code
        // make us a message
        p_.publish(msg)

    }
}

// Test
int main(){
    MyClass myClassWithMockPublisher(MockPublisher())

}

// actual

int main(){

    ros::init();
    ros::NodeHandle nh;

    MyClass myClassWithRealPublisher(RosPublisher(nh))

}
edit flag offensive delete link more

Question Tools

3 followers

Stats

Asked: 2020-02-17 08:24:16 -0500

Seen: 886 times

Last updated: Apr 10 '20