工作两年多了,一直采用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. Java使用Lettuce操作redis
  3. yjv是电缆还是电线_电力电缆YJV与BVV二者之间的区别是什么?
  4. 网络流之 最短增广路算法模板(SAP)
  5. 内置函数、匿名函数,递归函数
  6. mysql 5.6 gtid 主从_MySQL5.6基于GTID的主从复制
  7. java listnode 合并链表_剑指offer:合并两个排序的链表(Java)
  8. SpringBoot之Thymeleaf
  9. 《Python编程快速上手》8.9 实践项目
  10. [Linux] 通过shell给unix socket发送数据
  11. 【一文读懂】Contours Hierarchy ——opencv边界的继承结构,表格的提取,表格孔洞处理,空心形状结构的提取
  12. 计算机是学前端开发好还是后端开发好?
  13. C# GDI winfrom 图像转换椭圆形
  14. 计算机主机只有一块硬盘,电脑双硬盘只显示一个怎么办
  15. Python也有对象了哈哈哈哈哈哈嗝
  16. 软件中存在的技术风险
  17. js中的_poto_和prototype的问题
  18. 汇编SHR、SHL、SAR、SAL、ROL、ROR、RCL、RCR指令
  19. 超级高铁原型机现身迪拜,时速高达1200公里!
  20. 【课后习题】 线性代数第六版第一章 行列式 习题一

热门文章

  1. 2021年电气试验考试题库及电气试验考试资料
  2. 栈上内存溢出漏洞利用之Return Address
  3. DHCP服务器的配置与管理
  4. 基于Echarts实现可视化数据大屏董事会指标体系层级结构系统
  5. 奋斗的小蜗牛 南阳理工ACM 题目599
  6. 色彩理论之RGB(4)
  7. 远程登录服务协议简介
  8. 泰坦号潜艇事故给软件工程师的启示
  9. 如何在东八区的计算机上获取美国时间
  10. 2022年全球市场封闭式药物转移系统总体规模、主要企业、主要地区、产品和应用细分研究报告