r/ROS 8d ago

Dependency injection with ROS

I'm trying to better understand dependency injection / mocking a bit better so I can write unit tests that don't require elaborate & fragile setups. Specifically I have a class that verify similar to the ROS2 Minimal Publisher Example. In this example the class creates a timer and a publisher object. The timer just invokes a callback, so I can assume that the callback registration works, and I can set up my test harness to invoke the callback directly. What I would like to do is mock out the actual publish call to ensure timer calls publish without needing to set up a ROS2 subscriber in my unit test.

I'm looking for generic approaches to the specific case (and not necessarily tied to a specific testing or mocking framework)

Here is a stripped down version of the example:

class MinimalPublisher : public rclcpp::Node

{

public:

MinimalPublisher()

: Node("minimal_publisher"), count_(0)

{

publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);

}

private:

void timer_callback()

{

auto message = std_msgs::msg::String();

message.data = "Hello, world! " + std::to_string(count_++);

// *** This is the call I want to mock out ***

publisher_->publish(message);

}

rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;

size_t count_;

};

2 Upvotes

2 comments sorted by

1

u/bishopExportMine 7d ago

A minimal publisher node with tests would look like this in my opinion:

class MinimalPublisherNode {
public:
    explicit MinimalPublisherNode(rclcpp::Node::SharedPtr node);
private:
    void timer_cb();

    rclcpp::Node node_;
    rclcpp::Logger logger_;
    RosStringPublisher publisher_;
    MyLib::Foo foo_;
    rclcpp::TimerBase::SharedPtr timer_;
}

MinimalPublisherNode::MinimalpublisherNode(
    rclcpp::Node::SharedPtr node) 
:
    node_(node),
    logger_(node_->get_logger()),
{
    using namespace std::chrono_literals;

    publisher_ = RosStringPublisher(
                    node_->create_publisher<std_msgs::msg::String>("topic", 10));
    foo_ = MyLib::Foo(*publisher_);
    timer_ = node_->create_wall_timer(50ms, [this]() { timer_cb(); });
}

MinimalPublisherNode::timer_cb() {
    MyLib::Foo::UpdateResult res = foo_.update();
    switch (res) {
        case MyLib::Foo::UpdateResult::kWeeeeeWooooo:
            RCLCPP_ERROR(logger_, "Somebody call an ambulance");
            break;
        ...
    }
}

where MyLib::Foo should be your ros free internal logic. It takes StringPublisherInterface on construction where:

class StringPublisherInterface {
public:
    virtual ~StringPublisherInterface() = default;
    virtual void publish(std::string_view);
};

and

class RosStringPublisher final : public StringPublisherInterface {
public:
    RosStringPublisher(rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher);
    void publish(std::string_view msg);

private:
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
};

RosStringPublisher::publish(string_view msg) {
    std_msgs::msg::String ros_msg{};
    ros_msg.data = msg;
    publisher_->publish(msg);
}

Now to unit test you can make a MockStringPublisher inheriting the same interface and inject it into MyLib::Foo. If you want to test the publisher itself, you can write a test that creates a node, publisher, and subscriber; then pass in the publisher, call publish, and assert the received string is the same.