工作两年多了,一直采用TDD(测试驱动开发),刚开始觉得是反人类的方法论,后来在使用的过程中逐渐发现它的妙处。本文介绍了一些TDD的基本概念,并结合几个小需求进行实践。由于本人能力、精力有限,如有错误或者不当之处,还请各位提出宝贵的建议。

1. TDD原理

TDD流程.png

步骤:

  1. 先写测试代码,并执行,得到失败结果
  2. 写刚好让测试通过的代码,并通过测试用例
  3. 识别坏味道,重构代码,并保证测试通过
  4. 反复实行这个步骤,测试失败 -> 测试成功 -> 重构

三原则:

  1. 除非是为了使一个失败的用例通过,否则不允许编写任何代码
  2. 在一个单元测试中,只允许编写刚好能够导致失败的内容
  3. 只允许编写刚好能够使一个失败的用例通过的代码

详细的介绍详见参考文献1和2。

2. TDD实例

话不多说,下面通过一个实际的例子说明。由于最近在看STL,就实现一个简单的Array容器类,此例子可能不太贴切,但大体上是那么个过程。
该例子采用C++语言(为了方便,暂时将代码全部放在.h文件中)和谷歌的gtest测试框架(详见参考文献3)。完整的代码详见参考文献4,已经在Ubuntu 18.04调试通过,如有编译及运行问题,欢迎提出。

2.1 需求一

模仿STL,实现一个数据类型为int的Array类,且能够指定长度,此需求只要求实现其构造和析构函数。
按照TDD的步骤,我们首先写出测试用例(Test.cpp文件):

#include "IntArray.h"
#include "gtest/gtest.h"struct IntArrayTest : testing::Test
{};TEST_F(IntArrayTest, test_constructor)
{IntArray array{1};ASSERT_EQ(0, array[0]);
}

此时执行代码,编译是失败的。
然后写刚好让测试通过的代码(IntArray.h文件),并通过测试用例:

#ifndef INTARRAY_H_
#define INTARRAY_H_#include <cassert>struct IntArray
{IntArray() = default;IntArray(int len) : len(len){assert(this->len >= 0);if(this->len > 0){this->data = new int[this->len]{0};}}~IntArray(){delete[] this->data;}int& operator [](int idx) const{assert(idx >= 0 and idx < this->len);return this->data[idx];}private:int len;int* data;
};#endif

至此实现了需求一,且代码和用例编译、运行通过。此时代码没有出现明显的坏味道,暂时不需要重构。但是此时有一个比较大的问题,不知各位有没有发现,由于我们的关注点不在此处,暂时不做解释,下文会有说明及修改。

2.2 需求二

实现一个size()方法,该方法返回IntArray的长度;实现一个erase()方法,该方法可以清除IntArray的所有内容。
其实需求二是两个小需求,首先写出测试用例一:

TEST_F(IntArrayTest, test_func_size)
{IntArray array{3};ASSERT_EQ(3, array.size());
}

此时编译失败,再写刚好让测试通过的代码:

int size() const
{return this->len;
}

此时编译运行通过,再写第二个小需求的测试用例:

TEST_F(IntArrayTest, test_func_erase)
{IntArray array{3};ASSERT_EQ(3, array.size());array.erase();ASSERT_EQ(0, array.size());
}

再写刚好让测试通过的第二个小需求的代码:

void erase()
{delete[] this->data;this->data = nullptr;this->len = 0;
}

此时代码也没有出现明显的坏味道,暂时不需要重构。

2.3 需求三

实现一个类似于STL的insert()方法,要求能够实现Array任意位置的插入,包括起始、中间和结束位置。
此处我们先写出在中间位置插入的用例:

TEST_F(IntArrayTest, test_func_insert)
{IntArray array{2};for(int idx = 0; idx < 2; ++idx){array[idx] = idx;}ASSERT_EQ(0, array[0]);ASSERT_EQ(1, array[1]);int value = 3;int index = 1;array.insertBefore(value, index);ASSERT_EQ(3, array.size());ASSERT_EQ(0, array[0]);ASSERT_EQ(3, array[1]);ASSERT_EQ(1, array[2]);
}

再写出刚好能够使此测试用例通过的代码:

void insertBefore(int value, int index)
{assert(index >= 0 and index <= this->len);int* tmpData = new int[this->len + 1]{};for(int before = 0; before < index; ++before){tmpData[before] = this->data[before];} tmpData[index] = value;for(int after = index; after < this->len; ++after){tmpData[after + 1] = this->data[after];}delete[] this->data;this->data = tmpData;++this->len;
}

至此代码没有明显的坏味道,但是用例中出现了较多的ASSERT_EQ形式的重复。我们暂且忍一下,先实现我们其余的需求。
写出在起始位置插入的用例:

TEST_F(IntArrayTest, test_func_insert_at_begining)
{IntArray array{1};for(int idx = 0; idx < 1; ++idx){array[idx] = idx;}ASSERT_EQ(0, array[0]);int value = 3;array.insertAtBegining(value);ASSERT_EQ(2, array.size());ASSERT_EQ(3, array[0]);ASSERT_EQ(0, array[1]);
}

再写出刚好使此用例通过的代码:

void insertAtBegining(int value)
{insertBefore(value, 0);
}

我们可以发现ASSERT_EQ形式的重复在增多,我们选择继续忍(毕竟Copy and Paste多舒服),先实现我们其余的需求。
写出在结束位置插入的用例:

TEST_F(IntArrayTest, test_func_insert_at_end)
{IntArray array{1};for(int idx = 0; idx < 1; ++idx){array[idx] = idx;}ASSERT_EQ(0, array[0]);int value = 3;array.insertAtEnd(value);ASSERT_EQ(2, array.size());ASSERT_EQ(0, array[0]);ASSERT_EQ(3, array[1]);
}

再写出刚好使此用例通过的代码:

void insertAtEnd(int value)
{insertBefore(value, this->len);
}

2.4 代码重构

此时我们再也不能忍了,用例中重复的代码越来越多,代码重复是最严重的坏味道,必须消除。通过分析发现,重复代码无非两种类型,一种是IntArray的初始化,另一种是IntArray的校验。只需要将这些重复的代码提取到Fixture中即可,简单的重构如下:

struct IntArrayTest : testing::Test
{void initIntArr(IntArray& array) const{for(int idx = 0; idx < array.size(); ++idx){array[idx] = idx;}}void assertIntArr(const IntArray& array, const int num) const{ASSERT_EQ(num, array.size());for(int idx = 0; idx < array.size(); ++idx){ASSERT_EQ(array[idx], idx);}}
};

然后选择insert的用例重构如下:

TEST_F(IntArrayTest, test_func_insert)
{IntArray array{2};initIntArr(array);assertIntArr(array, 2);int value = 3;int index = 1;array.insertBefore(value, index);ASSERT_EQ(3, array.size());ASSERT_EQ(0, array[0]);ASSERT_EQ(3, array[1]);ASSERT_EQ(1, array[2]);
}TEST_F(IntArrayTest, test_func_insert_at_begining)
{IntArray array{1};initIntArr(array);assertIntArr(array, 1);int value = 3;array.insertAtBegining(value);ASSERT_EQ(2, array.size());ASSERT_EQ(3, array[0]);ASSERT_EQ(0, array[1]);
}TEST_F(IntArrayTest, test_func_insert_at_end)
{IntArray array{1};initIntArr(array);assertIntArr(array, 1);int value = 3;array.insertAtEnd(value);ASSERT_EQ(2, array.size());ASSERT_EQ(0, array[0]);ASSERT_EQ(3, array[1]);
}

此处的重构可能并不完美,但是重点是告诉大家要识别代码中的坏味道,并且主动去重构消除。

2.5 需求四

实现一个remove()方法,可以删除指定索引的元素。
相同的套路,首先写出用例:

TEST_F(IntArrayTest, test_func_remove)
{IntArray array{2};initIntArr(array);assertIntArr(array, 2);int index = 1;array.remove(index);ASSERT_EQ(1, array.size());ASSERT_EQ(0, array[0]);
}

相同的套路,再写出刚好能够使此测试用例通过的代码:

void remove(int index)
{assert(index >= 0 and index < this->len);if(this->len == 1){erase();       return ;}int* tmpData = new int[this->len]{};for(int before = 0; before < index; ++before){tmpData[before] = this->data[before];} for(int after = index + 1; after < this->len; ++after){tmpData[after - 1] = this->data[after];}delete[] this->data;this->data = tmpData;--this->len;
}

2.6 代码重构

此时我们可以发现在代码中出现了明显的重复,即insertBefore()函数和最新的remove()函数,最简单直接的方法是提取公共部分,如下:

...
struct IntArray
{void insertBefore(int value, int index){assert(index >= 0 and index <= this->len);int* tmpData = new int[this->len + 1]{};copyBeforeData(tmpData, index);tmpData[index] = value;for(int after = index; after < this->len; ++after){tmpData[after + 1] = this->data[after];}delete[] this->data;this->data = tmpData;++this->len;}void remove(int index){assert(index >= 0 and index < this->len);if(this->len == 1){erase();       return ;}int* tmpData = new int[this->len]{};copyBeforeData(tmpData, index);for(int after = index + 1; after < this->len; ++after){tmpData[after - 1] = this->data[after];}delete[] this->data;this->data = tmpData;--this->len;}private:void copyBeforeData(int* tmpData, const int index) const{for(int before = 0; before < index; ++before){tmpData[before] = this->data[before];}}
};
...

2.7 需求五

实现一个reallocate()函数,可以改变IntArray的size(),并且清空原有的元素。再实现一个resize()函数,可以改变IntArray的size(),但是保留原有的元素。
套路相同,对于此需求不再赘述,详见参考文献4。

2.8 需求六

将数据类型由int修改为double,实现上述的所有功能。
此时我们需要再重写一遍吗?当然不用。借助C++的泛型编码(模板机制,其实所有的STL都是采用模板实现,这样可以将数据和算法解耦;泛型编码和面向对象是C++重要的两个分支,只是它们考虑问题的方向不同)可以快速实现,详见参考文献4。

2.9 代码review

此时我们反观整体代码,有没有发现一些问题?提醒一下,内存方面的,其实内存管理一直是C++比较让人头痛的问题。是的,代码中有指针,若不实现显示的拷贝和赋值构造函数,会有浅拷贝的问题。具体详见参考文献5。

  1. 浅拷贝
    浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一块内存空间。当多个对象共用同一块内存资源时,若同一块资源释放多次,会发生崩溃或者内存泄漏。
  2. 深拷贝
    深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

最终实现的拷贝和赋值构造函数如下:

template<typename T>
struct Array
{Array(const Array& array){this->len = array.size();setData(array);}Array& operator=(const Array& array){if(this == &array){return *this;}delete[] this->data;this->len = array.size();setData(array);return *this;}private:void setData(const Array& array){if(array.size() > 0){this->data = new T[array.size()]{};for(int idx = 0; idx < array.size(); ++idx){this->data[idx] = array[idx];}}}
};

3. 总结

TDD只是一种实现方法,在某些场景下比较好用,在现实中我们要合理利用,没必要完完全全按照TDD的要求来做。
个人理解TDD算是敏捷开发的一种很好的实现方式,它的整体步骤如下:

  1. 先分解任务,分离关注点,实例化需求
  2. 写测试,只关注需求、程序的输入输出,不关心中间过程
  3. 写实现,不考虑别的需求,用最简单的方式实现当前这个小需求
  4. 手动测试一下,基本没什么问题,有问题再修复
  5. 重构,用手法消除代码里的坏味道
  6. 重复以上的步骤2, 3, 4和5
  7. 代码整洁且用例齐全,信心满满地提交

它有以下的优点:

  1. 提前澄清需求,明晰需求中的各种细节
  2. 小步快走,有问题能够及时修复
  3. 一个测试用例只关注一个点,降低开发者的负担
  4. 从整体来看,可以明显提升开发的效率

4. 参考文献

  1. tdd(测试驱动开发)的概述, https://blog.csdn.net/abchywabc/article/details/91351044
  2. 深度解读 - TDD(测试驱动开发), https://www.jianshu.com/p/62f16cd4fef3
  3. gtest的介绍和使用, https://blog.csdn.net/linhai1028/article/details/81675724
  4. https://github.com/mzh19940817/ArrayClass
  5. C++中深复制和浅复制(深拷贝和浅拷贝), https://zhangkaifang.blog.csdn.net/article/details/107865997

本文结合实例分享了TDD,文中可能有些许不当及错误之处,代码也没有用做到尽善尽美,欢迎大家批评指正,同时也欢迎大家评论、转载(请注明源出处),谢谢!

初识TDD(原理+实例)相关推荐

  1. JSP+JavaBean+Servlet工作原理实例…

    JSP+JavaBean+Servlet工作原理实例讲解 首先,JavaBean和Servlet虽都是Java程序,但是是完全不同的两个概念.引用mz3226960提出的MVC的概念,即M-model ...

  2. Redis--布隆过滤器--使用/原理/实例

    原文网址:Redis--布隆过滤器--使用/原理/实例_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍Redis的布隆过滤器的原理,优缺点,使用场景,实例. 布隆过滤器由n个Hash函数和一个 ...

  3. 分布式--雪花算法--使用/原理/实例

    原文网址:分布式--雪花算法--使用/原理/实例_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍分布式中的雪花算法.包括:用法.原理. 雪花算法用于生成全局的唯一ID. 使用时的注意事项 需要 ...

  4. iOS 用自签名证书实现 HTTPS 请求的原理实例讲解

    在16年的WWDC中,Apple已表示将从2017年1月1日起,所有新提交的App必须强制性应用HTTPS协议来进行网络请求.默认情况下非HTTPS的网络访问是禁止的并且不能再通过简单粗暴的向Info ...

  5. python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解

    2019独角兽企业重金招聘Python工程师标准>>> 1.前言 Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过 ...

  6. java 递归原理_Java中递归原理实例分析

    本文实例分析了Java中递归原理.分享给大家供大家参考.具体分析如下: 解释:程序调用自身的编程技巧叫做递归. 程序调用自身的编程技巧称为递归( recursion).递归做为一种算法在程序设计语言中 ...

  7. mybatis 传入id_想深入理解MyBatis架构及原理实例分析 把握这些就够了

    前言 MyBatis是目前非常流行的ORM框架,它的功能很强大,然而其实现却比较简单.优雅.本文主要讲述MyBatis的架构设计思路,并且讨论MyBatis的几个核心部件,然后结合一个select查询 ...

  8. 基于Angularjs+jasmine+karma的测试驱动开发(TDD)实例

    简介(摘自baidu) 测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法.它要求在编写某个功能的代码之前先编写测试代码,然 ...

  9. PHP Curl多线程原理实例详解

    来源:http://www.jb51.net/article/42826.htm 给各位介绍一下Curl多线程实例与原理.不对之处请指教 相信许多人对php手册中语焉不详的curl_multi一族的函 ...

最新文章

  1. 站在巨人的肩膀上“思考”问题,重在思考而不是拿来主义
  2. 《百面机器学习》---AI算法工程师求职必备“面经”
  3. Virtual 2004中安装DOS以及实现文件共享的方法
  4. MAC下安装多版本JDK和切换几种方式
  5. C++ Opengl 绘制字体源码
  6. 交换机端口灯闪烁频率一样_思创易控cetron-新品S2024GE 24口全千兆非网管交换机即将上市!...
  7. redis学习之三配置文件redis.conf 的含义
  8. Linux 启动失败 磁盘阵列,组建RAID5重启系统,出现md127的解决办法
  9. Python之路,Day2 - Python基础,列表,循环
  10. PHP搭建留言板,PHP搭建简易留言板
  11. 翻译: 欧洲伽利略卫星-全球导航卫星系统GNSS
  12. (转)Notepad++删除空白行
  13. 卡方线性趋势检验_趋势卡方检验
  14. node项目部署到云服务器
  15. RWS 负责任羊毛标准
  16. 服务器中毒了,无法登陆,开启拷贝恢复之路
  17. 量化面试题(逻辑题)
  18. 如何问一个人做什么工作_只是让你的人做他们的工作
  19. 一封来自程序猿的春节祝福
  20. 更改win7 64位系统调用IE时,默认64位IE的方法

热门文章

  1. RabbitMQ入门时出现的可笑异常..............
  2. D. DS哈希查找与增补(表尾插入)
  3. mssql与mysql语法区别_MSSQL与MySQL语法区别
  4. 2020T电梯修理证考试及T电梯修理考试试题
  5. python实现验证码识别_python实现图文验证码识别
  6. 加雨(雨天数据集制作)
  7. async awit 的用法思考
  8. Javascript setInterval多次,导致clearInterval失败
  9. Linux安装RabbitMQ详细教程Java使用
  10. echarts区域地图,自动高亮区域信息