How to do unit-testing with ROS?
What is unit testing?
First of all, it is important to understand what unit testing is. Generally speaking, a software project is divided into several interdependent functions. Initially, in a project, it is easy to make sure that they all work correctly, but as a project expands, and new functions are added, it becomes harder to certify that everything is still working, especially when new functionality is added. What if we created code to optimize this verification? This is the unit testing model.
This method consists of testing blocks of code separately to make sure that the logic that is written is correct and can be used in other parts of the code.
Why do it?
Unit testing is useful for speeding up the development process. When there is constant testing, it is easier to find bugs in the code, so developers can focus on extending the software rather than getting stuck fixing it. Another factor that should be pointed out is that often the programmer who writes blocks of code will also create the unit tests for this block. This results in slightly more work to develop the code, but the long-term benefits are great enough to compensate for this momentary delay.
Now that we know what a unit test is, we can get started.
We will use turtlebot3 in the Gazebo simulator to explain how we can implement unit tests in ROS projects. Turtlebot is a standard ROS platform, often used for educational purposes.
Take a look at this piece of code:
This is fairly complex code, and it might be a little hard to understand what is going on, but it is quite simple. This script implements a PID controller to move the turtlebot to a certain pre-defined point.
Now that we understand what is done, we can think about how to test this to see if it is correct.
One interesting test is whether when we run the program the turtlebot actually knows where the end goal is, in our code we use a parameter server to tell the turtlebot the end goal. We can then check if these parameters are being loaded correctly.
We defined the TestTurtlebot class as a subclass of unittest.TestCase, this is essential to be able to test functions separately using the unittest framework. Also, because of unittest, the functions that will be executed in the testing must have test_ as a prefix. If you are using different frameworks, take a look on the documentation of the framework you are using.
As for the test_param_server function, we are using the rospy.get_param() function to return the value of each of these threads. If none exist, the function will return None. We then test these local variables and check whether or not it is None. This is a very simple, but extremely important test. Checking the parameters is increasingly necessary as the project progresses.
Ok, now we have our first test ready!
But think with me, what is the point of knowing exactly where the goal is if the turtlebot can't reach it? Let's create a test to see if the turtlebot is really reaching its goal.
In this second test, using the values that we made sure were loaded into the parameter server, we will make a comparison of the initial and final (x,y) values in order to make sure that the turtlebot moves to the goal. In addition, we also have an error margin in the parameter server and we must take this into account during testing.
Here, we also see the interdependency of the tests, since if the first test fails, this test will also fail. In the same way, fixing a failing test will benefit the other tests.
It is worth noting that tests are not static after they are created. As more complex functionality is developed, the tests can be improved. In this case, taking into account a maximum time for the turtlebot to take to reach the goal can be a future optimization.
Finally, let's create a test to verify that the system's communication internally is correct. In the code "move2goal_turtlebot.py" we see that the turtlebot publishes its speed in the tópic /cmd_vel by the message type Twist.
Let's create a test that checks if the message is being sent in this topic while the program is running.
In the code we can see that the function velocityCallback does not have test_ as a prefix so it will not be executed in isolation during the test. The idea of this function is to create a ROS Subscriber that will be connected to /cmd_vel waiting for a message. If this message is not heard within 5 seconds, the code will not enter the velocityCallback function and the variable IsPublishing will remain False. At the end we check the value of this variable.
OK! We have created several interesting tests.... Now what? How to use these tests productively? For this we use the launch files, files that help automate several tasks with ROS and that deserve their own article.
If you wanna know more about ROS or ROS unit-testing:
The platform used:
Written by: Matheus Rodrigues and Arthur Sobrinho