ROS 2边学边练(43)-- 利用GTest写一个基本测试(C++)
前言
在ROS(Robot Operating System)中,gtest
(Google Test)是一个广泛使用的C++测试框架,用于编写和执行单元测试。这些测试可以验证ROS节点、服务和消息等的正确性和性能。
如果我们需要在写的包中添加测试,gtest是一个好的选择。在ROS中使用gtest进行单元测试的基本步骤如下:
- 编写测试代码:首先,我们需要编写使用gtest框架的C++测试代码。这些代码通常位于一个单独的ROS包中,并包含定义测试用例的
TEST
宏。 - 配置CMakeLists.txt:在我们的ROS包的
CMakeLists.txt
文件中,需要添加必要的gtest依赖项和编译指令。这通常包括find_package(catkin REQUIRED COMPONENTS roscpp rospy gtest)
来查找gtest包,以及catkin_add_gtest
来添加gtest测试目标。 - 构建和运行测试:使用ROS的构建系统(如catkin或colcon)来构建包。构建完成后,可以使用
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
添加下面的语句到package.xml
<test_depend>ament_cmake_gtest</test_depend>
CMakeLists.txt
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_teststd_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包,我们利用它来实现单元测试用例(基于客户端/服务端的服务通信方式)。
创建service包 (可选)
如果之前的cpp_srvcli包还在的话,此service包可以不用再创建。
进入工作空间src路径下,执行如下命令创建service包:
$ros2 pkg create --build-type ament_cmake --license Apache-2.0 service --dependencies rclcpp tutorial_interfaces
src/service.cpp
在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, // CHANGEstd::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response> response) // CHANGE
{response->sum = request->a + request->b + request->c; // CHANGERCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld", // CHANGErequest->a, request->b, request->c); // CHANGERCLCPP_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"); // CHANGErclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service = // CHANGEnode->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add); // CHANGERCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints."); // CHANGErclcpp::spin(node);rclcpp::shutdown();
}
创建client包
进入工作空间根路径src,执行如下命令创建client包:
$ros2 pkg create --build-type ament_cmake --license Apache-2.0 client --dependencies rclcpp tutorial_interfaces
include/client/client.h
在client包的include路径,创建client.h,内容如下:
// client.h
#ifndef CLIENT_H
#define CLIENT_Hclass ClientHandler
{public:ClientHandler();~ClientHandler();bool sendParams(int argc, char **argv);
};
#endif
include/client/params.h
// params.h
#ifndef PARAMS_H
#define PARAMS_Hextern int my_argc;
extern char** my_argv;#endif
src/client.cpp
// 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;
}
src/main.cpp
// main.cpp
#include "../include/client/client.h"int main(int argc, char **argv){// 注意这里: C++ 编译器把不带参数的构造函数优先认为是一个函数声明ClientHandler client{};client.sendParams(argc, argv);
}
单元测试文件
test/clientTest.cpp
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));
}
test/main.cpp
// 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();
}
CMakeLists.txt
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(SRCsrc/client.cppsrc/main.cpp
)add_executable(client ${SRC})
ament_target_dependencies(clientrclcpp tutorial_interfaces)# 5. 添加当前项目中的头文件 注意有顺序的要求,不能乱
target_include_directories(clientPRIVATE ${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(TESTtest/main.cpptest/clientTest.cpp)# 生成加入gtest的test执行文件。${PROJECT_NAME}_test为自定义的test执行文件名称;test/demo_test.cpp为test源码路径# 注意这里导包的时候,不再需要将 .h 文件导入进来,因为在 client.cpp中已经导入了我们需要使用到的.h文件# 另外,注意这里不能导入开发代码中的 main.cpp,因为已经有了一个测试的main.cppament_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(TARGETSclientDESTINATION 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()
package.xml
<?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包
回到工作空间根路径,构建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,至此一个客户端的单元测试完成。
对于单元测试的知识点其实挺多的,一时确实无法都掌握,一篇内容也不能都概括,当前我们也只需了解一个基本的用法即可,后面随着实际碰到的问题越来越多,再根据具体的问题钻研下去,掌握到的知识点也就会水涨船高了。
本篇完。
相关文章:

ROS 2边学边练(43)-- 利用GTest写一个基本测试(C++)
前言 在ROS(Robot Operating System)中,gtest(Google Test)是一个广泛使用的C测试框架,用于编写和执行单元测试。这些测试可以验证ROS节点、服务和消息等的正确性和性能。 如果我们需要在写的包中添加测试&…...

3.整数运算
系列文章目录 信息的表示和处理 : Information Storage(信息存储)Integer Representation(整数表示)Integer Arithmetic(整数运算)Floating Point(浮点数) 文章目录 系列文章目录前…...
uri.getQueryParameters(name)返回一个列表(List)
uri.getQueryParameters(name)返回一个列表(List)而不是单个值的原因在于URI(统一资源标识符)中查询参数(query parameters)的设计允许同一个名称(name)对应多个值。这意味着一个查询…...

鸿蒙ArkUI开发:常用布局【主轴】
ArkUI中常用布局容器 线性布局(Row/Column) 线性布局的子元素在线性方向上(水平方向和垂直方向)依次排列线性布局容器包括[Row]和[Column]。Column容器内子元素按照垂直方向排列,Row容器内子元素按照水平方向排列开发…...
Spring Security 入门 2
1.项目实战 就以RuoYi-Vue 为例吧,主要以下几点原因: 基于 Spring Security 实现。 基于 RBAC 权限模型,并且支持动态的权限配置。 基于 Redis 服务,实现登录用户的信息缓存。 前后端分离。同时前端采用 Vue ,相对来…...

C++初阶学习第七弹——探索STL奥秘(二)——string的模拟实现
标准库中的string:C初阶学习第六弹——string(1)——标准库中的string类-CSDN博客 前言: 在前面我们已经学习了如何使用标准库中的string类,但作为一个合格的程序员,我们不仅要会用,还要知道如…...

5.nginx常用命令和日志定时切割
一. nginx常用的相关命令介绍 1.强制关闭nginx: ./nginx -s stop 2.优雅的关闭nginx: ./nginx -s quit 3.检查配置文件是否正确: ./nginx -t 4.查看nginx版本: ./nginx -v 5.查看nginx版本相关的配置环境信息:./nginx -V 6.nginx帮助信…...

Redis-详解(基础)
文章目录 什么是Redis?用Redis的特点?用Redis可以实现哪些功能?Redis的常用数据类型有哪些?Redis的常用框架有哪些?本篇小结 更多相关内容可查看 什么是Redis? Redis(Remote DictionaryServer)是一个开源…...

记录minio的bug(Object name contains unsupported characters.)
场景是我将后端服务从121.xxx.xxx.xxx服务器上转移到了另一台服务器10.xxx.xxx.xxx 但图片都还在121.xxx.xxx.xxx服务器上,同样我10.xxx.xxx.xxx也安装了minio并且我的后端服务配置的minio地址也是10.xxx.xxx.xxx 此时有一个业务通过minio客户端获取图片…...
【嵌入式开发 Linux 常用命令系列 7.6 -- sed 替换指定字符串】
请阅读【嵌入式开发学习必备专栏】 文章目录 sed 替换指定字符串 sed 替换指定字符串 背景: 找到当前目录下所有的.h 和 .c 文件 将他们中的字符 print_log替换为 demo_log 可以使用find命令结合sed命令在Linux环境下完成这项任务。下面是一个命令行示例ÿ…...

C++语言的字符数组
存放字符数据的数组是字符数组,字符数组中的一个元素存放一个字符。字符数组具有数组的共同属性。 1. 声明一个字符数组 char c[5]; 2. 字符数组赋值方式 (1)为数组元素逐一赋值 c[0]H c[1]E c[2]L c[3]L c[4]O (2&…...
24届电信红队实习生面经
sql注入的一些:原理、打的靶场的常见绕过、问了一些函数 (load_file、 outfile这些)、后利用 (mysql的udf提权的原理、条件、利用、其他像mssql这些数据库的提权手段、这些就没细问了, 就问有哪些方式; 问了有没有实战遇到mysql的…...

linux下使用jexus部署aspnet站点
1.运行环境 Centos 7 安装dos2unix工具 yum install dos2unix 安装jexus curl https://jexus.org/release/x64/install.sh|sudo sh2.网站部署 2.1. 将windows下的网站发布包Msc_qingdao_admin.zip上传到linux中, 然后解压后放入/var/www(没有则创建)目录下 r…...

代码随想录训练营Day 27|理论基础、力扣 77. 组合
1.理论基础 题目链接/文章讲解:代码随想录 视频讲解:带你学透回溯算法(理论篇)| 回溯法精讲!_哔哩哔哩_bilibili 来自代码随想录的网站: void backtracking(参数) {if (终止条件) {存放结果;return;}for (…...

Spring框架深度解析:打造你的Java应用梦工厂
想要在Java企业级应用开发中大展身手?Spring框架的核心容器是你不可或缺的伙伴! 文章目录 一. 引言1.1 介绍Spring框架的重要性1.2 阐述核心容器在Spring框架中的作用1.3 故事开端 二. 背景介绍2.1 描述Spring框架的发展历程2.2 概述Spring框架的主要特点…...
Python 正则表达式(一)
文章目录 概念正则函数match函数正则表达式修饰符意义: 常用匹配符限定符原生字符串边界字符 概念 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个…...
Cocos Creator 3.8.x报错:5302
在小游戏加载某个bundle后,如果报以下错误: 5302:Can not find class %s 说明bundle中某个预制件*.prefab引用了未加载的bundle的资源。 解决方法有两个: 1、将引用的资源移到预制件*.prefab相同的bundle下; 2、将…...

网页如何集成各社区征文活动
Helllo , 我是小恒 由于我需要腾讯云社区,稀土掘金以及CSDN的征文活动RSS,找了一下没发现,所以使用GET 请求接口对网页定时进行拉取清洗,甚至无意间做了一个简单的json格式API 最终网址:hub.liheng.work API:http://hub.liheng.wo…...

【知识碎片】2024_05_13
本文记录了两道代码题【自除数】和【除自身以外数组的乘积】(利用了前缀积和后缀积,值得再看),第二部分记录了关于指针数组和逗号表达式的两道选择题。 每日代码 自除数 . - 力扣(LeetCode) /*** Note: T…...
Day53代码随想录动态规划part13:300.最长递增子序列、674. 最长连续递增序列、718. 最长重复子数组
Day52 动态规划part13 300.最长递增子序列 leetcode链接:300. 最长递增子序列 - 力扣(LeetCode) 题意:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除&a…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...

如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...

html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...
WEB3全栈开发——面试专业技能点P7前端与链上集成
一、Next.js技术栈 ✅ 概念介绍 Next.js 是一个基于 React 的 服务端渲染(SSR)与静态网站生成(SSG) 框架,由 Vercel 开发。它简化了构建生产级 React 应用的过程,并内置了很多特性: ✅ 文件系…...
DAY 26 函数专题1
函数定义与参数知识点回顾:1. 函数的定义2. 变量作用域:局部变量和全局变量3. 函数的参数类型:位置参数、默认参数、不定参数4. 传递参数的手段:关键词参数5 题目1:计算圆的面积 任务: 编写一…...

JUC并发编程(二)Monitor/自旋/轻量级/锁膨胀/wait/notify/锁消除
目录 一 基础 1 概念 2 卖票问题 3 转账问题 二 锁机制与优化策略 0 Monitor 1 轻量级锁 2 锁膨胀 3 自旋 4 偏向锁 5 锁消除 6 wait /notify 7 sleep与wait的对比 8 join原理 一 基础 1 概念 临界区 一段代码块内如果存在对共享资源的多线程读写操作…...