欢迎光临散文网 会员登陆 & 注册

TDD(测试驱动开发)的意义在哪?

2022-06-08 15:03 作者:西湖大学空中机器人  | 我要投稿

在之前的文章中,我们讨论了如何使用面向对象和设计模式来设计算法。然而在实际的算法开发中,算法经常需要迭代和重构。那如何能保证算法的代码质量呢?

为了方便理解,我们以3D投影算法为例。该算法将物体在世界坐标系下的坐标转换为在相机图像中的像素坐标,涉及三部分:

① 世界坐标系转换至相机坐标系

② 相机坐标系转换至图像坐标系

③ 图像坐标系转换到像素坐标系

代码如下(省略了算法细节):

void WorldToCamera(const WorldCoordinates& world_coord,                   CameraCoordinates& camera_coord)

{  

// ...  

// assign value to camera_coord

}

void CameraToImage(const CameraCoordinates& camera_coord,

                ImageCoordinates& image_coord)

{

// ...  

// assign value to image_coord

}

void ImageToPixel(const ImageCoordinates& image_coord,

               PixelCoordinates& pixel_coord)

{

// ...  

// assign value to pixel_coord

}

void WorldToPixel(const WorldCoordinates& world_coord,

               PixelCoordinates& pixel_coord)

{

CameraCoordinates camera_coord;  

ImageCoordinates image_coord;  

WorldToCamera(world_coord, camera_coord);  

CameraToImage(camera_coord, image_coord);  

ImageToPixel(image_coord, pixel_coord);

}

在原有基础上,我们想优化代码。当修改完代码后发现,最终坐标转换结果错误。但是我们无法确定具体是算法中的哪一步骤出了问题。所以我们就开始了漫长的调试阶段。但是,这个过程是很浪费时间的。因为你不知道问题出在哪里,所以你就好像是一只“无头苍蝇”一样在代码中到处调试,添加断点、大量的注释以及打印信息,甚至因着不够小心,误删了或是修改了重要的源代码而没有察觉。导致整个调试过程困难且低效,甚至你的“调试”会增加更多的bug。那么,如何来解决这个问题呢?

▌ TDD

TDD (Test Driven Development),测试驱动开发是一种以测试为优先的设计方法论。通过编写单元测试用例,保证复杂系统/算法的开发和重构质量。


简单来说, TDD就是:红,绿,重构。

①首先,非常重要的一点就是“测试先行”。在编写程序之先,首先写好测试程序,包括确定测试用例。

②然后才是编写代码。当代码初步完成时,运行测试程序来验证算法。此时你会发现, 代码不能通过所有的测试。通过分析这些用例无法通过的原因,修改代码直至通过全部测试。

③而后便是不断地重构代码。在此过程中,每次修改代码都需要通过测试程序的验证。

▌ Gtest

ROS内置了gtest模块,我们可用gtest来实现TDD。gtest又称GoogleTest, 是由Google发布的C++单元测试框架。现在我们为上述算法编写一个测试程序。

// Bring in my package's API, which is what I'm testing

#include <tdd/coordinate.h>

// Bring in gtest

#include <ros/ros.h>

#include <gtest/gtest.h>

TEST(TestSuite, testWorldToCamera)

{  

WorldCoordinates world_coord = {200, 300, -20};  

CameraCoordinates camera_coord;  

WorldToCamera(world_coord, camera_coord);  

EXPECT_EQ(100, camera_coord.x);

EXPECT_EQ(-80, camera_coord.y);  

EXPECT_EQ(-20, camera_coord.z);

}

TEST(TestSuite, testCameraToImage)

{  

CameraCoordinates camera_coord = {100, -80, -20};  

ImageCoordinates image_coord;  

CameraToImage(camera_coord, image_coord);  

EXPECT_EQ(100, image_coord.x);  

EXPECT_EQ(-80, image_coord.y);

}

TEST(TestSuite, testImageToPixel)

{  

ImageCoordinates image_coord = {100, -80};  

PixelCoordinates pixel_coord;

ImageToPixel(image_coord, pixel_coord);  

EXPECT_EQ(400, pixel_coord.x);  

EXPECT_EQ(300, pixel_coord.y);

}

TEST(TestSuite, testWorldToPixel)

{  

WorldCoordinates world_coord = {200, 300, -20};  

PixelCoordinates pixel_coord;  

WorldToPixel(world_coord, pixel_coord);  

EXPECT_EQ(400, pixel_coord.x);  

EXPECT_EQ(300, pixel_coord.y);

}

// Run all the tests that were declared with TEST()

int main(int argc, char **argv){  

testing::InitGoogleTest(&argc, argv);  

ros::init(argc, argv, "tester");  

ros::NodeHandle nh;  return RUN_ALL_TESTS();

}

运行测试程序后,若是代码能通过所有测试用例,将输出以下结果:

若是算法中某一部分实现有错误,比如说ImageToPixel坐标转换错误,将输出以下结果:

该输出结果告诉我们,WorldToCamera, CameraToImage坐标转换正确,而ImageToPixle坐标转换错误。错误值出现在坐标的y值上,期望理论值为300,而实际计算值为200;错误的代码在源文件http://test_coordinate.cc中的41行。

gtest的断言

断言在对一个类或函数进行测试时经常会使用到。当一个断言,如上述的

失败时,屏幕上会输出该代码所在的源文件及其所在的位置行号,以及错误信息。常见的断言还有,布尔值检查,数值型数据检查,字符串比较,异常检查,浮点型检查,类型检查等。

快速反馈

通过运行测试程序输出的信息,我们能快速得到反馈,从而得知算法中的哪一步骤出现问题。表面上看,编写测试程序增加了代码量;实际上在后续的算法开发和重构阶段会为我们节省大量的时间。如上述所示,我们快速定位到ImageToPixle出现问题,只需要关注该部分代码而无需再去检查其他部分的代码。并且,每一次测试我们都不需要去做额外的工作,如准备测试数据,启动节点等,这些都包含在测试程序里。

确保代码质量

通过合适且足够的测试用例来覆盖算法的各步骤所涉及的各种情况,为算法提供全方位的质量保证。

▌ 总结

在这篇文章中,我们介绍如何使用gtest在ROS中实现了TDD,并将其运用在算法开发和重构中,提高了开发效率并保证了代码质量。关于更多gtest的使用,可查阅参考资料。


参考资料

1. https://google.github.io/googletest/

2. http://wiki.ros.org/gtest


本文共1455字

由西湖大学智能无人系统实验室工程师陈华奔原创

申请文章授权请联系后台相关运营人员

▌微信公众号:空中机器人前沿

▌知乎:空中机器人前沿(本文链接:https://www.zhihu.com/question/329784671/answer/2483642856)

▌Youtube:Aerial robotics @ Westlake University

▌实验室网站:https://shiyuzhao.westlake.edu.cn/


TDD(测试驱动开发)的意义在哪?的评论 (共 条)

分享到微博请遵守国家法律