赞
踩
在ROS(Robot Operating System)中,gtest
(Google Test)是一个广泛使用的C++测试框架,用于编写和执行单元测试。这些测试可以验证ROS节点、服务和消息等的正确性和性能。
如果我们需要在写的包中添加测试,gtest是一个好的选择。在ROS中使用gtest进行单元测试的基本步骤如下:
TEST
宏。CMakeLists.txt
文件中,需要添加必要的gtest依赖项和编译指令。这通常包括find_package(catkin REQUIRED COMPONENTS roscpp rospy gtest)
来查找gtest包,以及catkin_add_gtest
来添加gtest测试目标。rostest
或ctest
等命令来运行gtest测试。如果我们需要在一个基于ament_cmake的功能包中添加单元测试,可以借鉴如下方法流程。
我们从test/tutorial_test.cpp里面的代码开始。
- #include <gtest/gtest.h>
-
- TEST(package_name, a_first_test)
- {
- ASSERT_EQ(4, 2 + 2);
- }
-
- int main(int argc, char** argv)
- {
- testing::InitGoogleTest(&argc, argv);
- return RUN_ALL_TESTS();
- }
添加下面的语句到package.xml
<test_depend>ament_cmake_gtest</test_depend>
- if(BUILD_TESTING)
- find_package(ament_cmake_gtest REQUIRED)
- ament_add_gtest(${PROJECT_NAME}_tutorial_test test/tutorial_test.cpp)
- target_include_directories(${PROJECT_NAME}_tutorial_test PUBLIC
- $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
- $<INSTALL_INTERFACE:include>
- )
- ament_target_dependencies(${PROJECT_NAME}_tutorial_test
- std_msgs
- )
- target_link_libraries(${PROJECT_NAME}_tutorial_test name_of_local_library)
- endif()
我们将测试代码涵括在if/endif语句块中,当宏BUILD_TESTING为真时,即编译测试模块。其中的ament_add_gtest函数很像add_executable,所以我们同样需要调用target_include_directories,ament_target_dependencies和target_link_libraries来包括我们需要的文件。
参见上一篇博文。其实在之前创建的ros2_ws工作空间路径下(我们已经在此空间下练习了很多例子),我们可以直接运行colcon test命令,就会将该工作空间下的所有包都执行了次单元测试(如果需要单独测试某个包也可以colcon test --package-select <package_name>),也可以通过上一篇博文中的colcon test-result --all命令进行查看结果。
在工作空间根路径下的build文件夹下,对应的包路径下会生成Testing文件夹,里面包含了一些单元测试的结果或记录文件。
本篇稍显粗略,不够详实。一个完整的例子可以参考这里,将新包放入到工作空间src路径下,依次执行如下操作:
编译构建
$colcon build --package-select minimal_integration_test
运行测试用例
$colcon test --packages-select minimal_integration_test
此例参考了cnblog上面的一篇博文。
我们在前面的教程中已经创建过tutorial_interfaces功能包及cpp_srvcli包,我们利用它来实现单元测试用例(基于客户端/服务端的服务通信方式)。
如果之前的cpp_srvcli包还在的话,此service包可以不用再创建。
进入工作空间src路径下,执行如下命令创建service包:
$ros2 pkg create --build-type ament_cmake --license Apache-2.0 service --dependencies rclcpp tutorial_interfaces
在service包的src路径下创建service.cpp文件,内容如下:
- #include "rclcpp/rclcpp.hpp"
- #include "tutorial_interfaces/srv/add_three_ints.hpp" // CHANGE
-
- #include <memory>
-
- void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request, // CHANGE
- std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response> response) // CHANGE
- {
- response->sum = request->a + request->b + request->c; // CHANGE
- RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld", // CHANGE
- request->a, request->b, request->c); // CHANGE
- RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
- }
-
- int main(int argc, char **argv)
- {
- rclcpp::init(argc, argv);
-
- std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server"); // CHANGE
-
- rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service = // CHANGE
- node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add); // CHANGE
-
- RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints."); // CHANGE
-
- rclcpp::spin(node);
- rclcpp::shutdown();
- }
-

进入工作空间根路径src,执行如下命令创建client包:
$ros2 pkg create --build-type ament_cmake --license Apache-2.0 client --dependencies rclcpp tutorial_interfaces
在client包的include路径,创建client.h,内容如下:
- // client.h
- #ifndef CLIENT_H
- #define CLIENT_H
-
- class ClientHandler
- {
- public:
- ClientHandler();
- ~ClientHandler();
- bool sendParams(int argc, char **argv);
- };
- #endif
- // params.h
- #ifndef PARAMS_H
- #define PARAMS_H
-
- extern int my_argc;
- extern char** my_argv;
-
- #endif
- // client.cpp
- #include "rclcpp/rclcpp.hpp"
- #include "tutorial_interfaces/srv/add_three_ints.hpp"
- #include "../include/client/client.h"
-
- #include <chrono>
- #include <cstdlib>
- #include <memory>
- #include<vector>
- using namespace std;
-
- using namespace std::chrono_literals;
-
- // 构造函数
- ClientHandler::ClientHandler(){
-
- }
-
- // 析构函数
- ClientHandler::~ClientHandler(){
-
- }
-
-
- // 普通函数——发送参数
- bool ClientHandler::sendParams(int argc, char **argv)
- {
- rclcpp::init(argc, argv);
-
- if (argc != 4) {
- RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_three_ints_client X Y Z");
- return false;
- }
-
- std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client");
- rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client =
- node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints");
-
- auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>();
- request->a = atoll(argv[1]);
- request->b = atoll(argv[2]);
- request->c = atoll(argv[3]);
-
- while (!client->wait_for_service(1s)) {
- if (!rclcpp::ok()) {
- RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
- return false;
- }
- RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
- }
-
- auto result = client->async_send_request(request);
- // Wait for the result.
- if (rclcpp::spin_until_future_complete(node, result) ==
- rclcpp::FutureReturnCode::SUCCESS)
- {
- RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
- } else {
- RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_three_ints");
- }
-
- rclcpp::shutdown();
- return true;
- }

- // main.cpp
- #include "../include/client/client.h"
-
- int main(int argc, char **argv){
- // 注意这里: C++ 编译器把不带参数的构造函数优先认为是一个函数声明
- ClientHandler client{};
- client.sendParams(argc, argv);
- }
client包根路径下创建test文件夹。
- // clientTest.cpp
- #include "gtest/gtest.h"
-
- #include "../include/client/client.h"
- #include "../include/client/params.h"
-
- TEST(ClientHandler, sendParams)
- {
- // 测试的时候的交互方式也不能改变,既然client实际的效果是在命令行输入参数,
- // 那这里也是这样的效果
- ClientHandler client{};
- EXPECT_EQ(true, client.sendParams(my_argc, my_argv));
- }
- // main.cpp
- #include <gtest/gtest.h>
- // #include <gmock/gmock.h>
-
- int my_argc;
- char** my_argv;
-
- int main(int argc, char** argv) {
- // ::testing::InitGoogleMock(&argc, argv);
- // 注意这里使用的是Gtest,不是Gmock
- ::testing::InitGoogleTest(&argc, argv);
- // Runs all tests using Google Test.
- my_argc = argc;
- my_argv = argv;
- return RUN_ALL_TESTS();
- }

- cmake_minimum_required(VERSION 3.8)
- project(client)
-
- if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
- add_compile_options(-Wall -Wextra -Wpedantic)
- endif()
-
- # find dependencies
- find_package(ament_cmake REQUIRED)
- find_package(rclcpp REQUIRED)
- find_package(tutorial_interfaces REQUIRED)
-
- set(SRC
- src/client.cpp
- src/main.cpp
- )
-
- add_executable(client
- ${SRC}
- )
- ament_target_dependencies(client
- rclcpp tutorial_interfaces)
-
- # 5. 添加当前项目中的头文件 注意有顺序的要求,不能乱
- target_include_directories(client
- PRIVATE
- ${PROJECT_SOURCE_DIR}/include
- )
-
- # 如果是测试代码
- if(BUILD_TESTING)
- find_package(ament_lint_auto REQUIRED)
- # 加入gtest包
- find_package(ament_cmake_gtest REQUIRED)
- # the following line skips the linter which checks for copyrights
- # uncomment the line when a copyright and license is not present in all source files
- # set(ament_cmake_copyright_FOUND TRUE)
- # the following line skips cpplint (only works in a git repo)
- # uncomment the line when this package is not in a git repo
- # set(ament_cmake_cpplint_FOUND TRUE)
- set(TEST
- test/main.cpp
- test/clientTest.cpp
- )
- # 生成加入gtest的test执行文件。${PROJECT_NAME}_test为自定义的test执行文件名称;test/demo_test.cpp为test源码路径
- # 注意这里导包的时候,不再需要将 .h 文件导入进来,因为在 client.cpp中已经导入了我们需要使用到的.h文件
- # 另外,注意这里不能导入开发代码中的 main.cpp,因为已经有了一个测试的main.cpp
- ament_add_gtest(${PROJECT_NAME}_test ${TEST} src/client.cpp)
- # 务必注意这里需要添加的依赖包
- ament_target_dependencies(${PROJECT_NAME}_test rclcpp tutorial_interfaces)
-
- install(TARGETS
- ${PROJECT_NAME}_test
- # 将生成的test执行文件安装到DESTINATION后的路径下
- DESTINATION lib/${PROJECT_NAME})
- ament_lint_auto_find_test_dependencies()
- endif()
-
- install(TARGETS
- client
- DESTINATION lib/${PROJECT_NAME})
-
- # 设置编译构建类型为 调试 模式
- set(CMAKE_BUILD_TYPE Debug)
- # 生成覆盖率文件
- set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage")
- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage")
-
- ament_package()

- <?xml version="1.0"?>
- <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
- <package format="3">
- <name>client</name>
- <version>0.0.0</version>
- <description>client test</description>
- <maintainer email="mike@qq.com">zhi</maintainer>
- <license>TODO: License declaration</license>
-
- <buildtool_depend>ament_cmake</buildtool_depend>
-
- <depend>rclcpp</depend>
- <depend>tutorial_interfaces</depend>
-
- <test_depend>ament_lint_auto</test_depend>
- <test_depend>ament_lint_common</test_depend>
-
- <export>
- <build_type>ament_cmake</build_type>
- </export>
- </package>

回到工作空间根路径,构建client.
$colcon build --packages-select client
在工作空间根路径build/client路径下(install/client/lib路径下同样生成),如果正常,会有client_test可执行文件生成。
我们可以直接运行前面创建的cpp_srvcli包里的server节点作为服务端(或者构建上面写的service包)。另开一个终端,运行之前记得配置下环境(source install/setup.bash)。
$ros2 run cpp_srvcli server
再运行client_test。
$./build/client/client_test 23 3 5
上图右侧的终端窗口可以看到测试PASSED,至此一个客户端的单元测试完成。
对于单元测试的知识点其实挺多的,一时确实无法都掌握,一篇内容也不能都概括,当前我们也只需了解一个基本的用法即可,后面随着实际碰到的问题越来越多,再根据具体的问题钻研下去,掌握到的知识点也就会水涨船高了。
本篇完。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。