初识TDD(原理+实例)
工作两年多了,一直采用TDD(测试驱动开发),刚开始觉得是反人类的方法论,后来在使用的过程中逐渐发现它的妙处。本文介绍了一些TDD的基本概念,并结合几个小需求进行实践。由于本人能力、精力有限,如有错误或者不当之处,还请各位提出宝贵的建议。
1. TDD原理
TDD流程.png
步骤:
- 先写测试代码,并执行,得到失败结果
- 写刚好让测试通过的代码,并通过测试用例
- 识别坏味道,重构代码,并保证测试通过
- 反复实行这个步骤,测试失败 -> 测试成功 -> 重构
三原则:
- 除非是为了使一个失败的用例通过,否则不允许编写任何代码
- 在一个单元测试中,只允许编写刚好能够导致失败的内容
- 只允许编写刚好能够使一个失败的用例通过的代码
详细的介绍详见参考文献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。
- 浅拷贝
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一块内存空间。当多个对象共用同一块内存资源时,若同一块资源释放多次,会发生崩溃或者内存泄漏。 - 深拷贝
深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
最终实现的拷贝和赋值构造函数如下:
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算是敏捷开发的一种很好的实现方式,它的整体步骤如下:
- 先分解任务,分离关注点,实例化需求
- 写测试,只关注需求、程序的输入输出,不关心中间过程
- 写实现,不考虑别的需求,用最简单的方式实现当前这个小需求
- 手动测试一下,基本没什么问题,有问题再修复
- 重构,用手法消除代码里的坏味道
- 重复以上的步骤2, 3, 4和5
- 代码整洁且用例齐全,信心满满地提交
它有以下的优点:
- 提前澄清需求,明晰需求中的各种细节
- 小步快走,有问题能够及时修复
- 一个测试用例只关注一个点,降低开发者的负担
- 从整体来看,可以明显提升开发的效率
4. 参考文献
- tdd(测试驱动开发)的概述, https://blog.csdn.net/abchywabc/article/details/91351044
- 深度解读 - TDD(测试驱动开发), https://www.jianshu.com/p/62f16cd4fef3
- gtest的介绍和使用, https://blog.csdn.net/linhai1028/article/details/81675724
- https://github.com/mzh19940817/ArrayClass
- C++中深复制和浅复制(深拷贝和浅拷贝), https://zhangkaifang.blog.csdn.net/article/details/107865997
本文结合实例分享了TDD,文中可能有些许不当及错误之处,代码也没有用做到尽善尽美,欢迎大家批评指正,同时也欢迎大家评论、转载(请注明源出处),谢谢!
初识TDD(原理+实例)相关推荐
- JSP+JavaBean+Servlet工作原理实例…
JSP+JavaBean+Servlet工作原理实例讲解 首先,JavaBean和Servlet虽都是Java程序,但是是完全不同的两个概念.引用mz3226960提出的MVC的概念,即M-model ...
- Redis--布隆过滤器--使用/原理/实例
原文网址:Redis--布隆过滤器--使用/原理/实例_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍Redis的布隆过滤器的原理,优缺点,使用场景,实例. 布隆过滤器由n个Hash函数和一个 ...
- 分布式--雪花算法--使用/原理/实例
原文网址:分布式--雪花算法--使用/原理/实例_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍分布式中的雪花算法.包括:用法.原理. 雪花算法用于生成全局的唯一ID. 使用时的注意事项 需要 ...
- iOS 用自签名证书实现 HTTPS 请求的原理实例讲解
在16年的WWDC中,Apple已表示将从2017年1月1日起,所有新提交的App必须强制性应用HTTPS协议来进行网络请求.默认情况下非HTTPS的网络访问是禁止的并且不能再通过简单粗暴的向Info ...
- python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解
2019独角兽企业重金招聘Python工程师标准>>> 1.前言 Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过 ...
- java 递归原理_Java中递归原理实例分析
本文实例分析了Java中递归原理.分享给大家供大家参考.具体分析如下: 解释:程序调用自身的编程技巧叫做递归. 程序调用自身的编程技巧称为递归( recursion).递归做为一种算法在程序设计语言中 ...
- mybatis 传入id_想深入理解MyBatis架构及原理实例分析 把握这些就够了
前言 MyBatis是目前非常流行的ORM框架,它的功能很强大,然而其实现却比较简单.优雅.本文主要讲述MyBatis的架构设计思路,并且讨论MyBatis的几个核心部件,然后结合一个select查询 ...
- 基于Angularjs+jasmine+karma的测试驱动开发(TDD)实例
简介(摘自baidu) 测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法.它要求在编写某个功能的代码之前先编写测试代码,然 ...
- PHP Curl多线程原理实例详解
来源:http://www.jb51.net/article/42826.htm 给各位介绍一下Curl多线程实例与原理.不对之处请指教 相信许多人对php手册中语焉不详的curl_multi一族的函 ...
最新文章
- 计算机专业每年都有国企招老吗,这十大专业在国企中最受欢迎,待遇高、前景好,有你的专业吗?...
- Java使用Lettuce操作redis
- yjv是电缆还是电线_电力电缆YJV与BVV二者之间的区别是什么?
- 网络流之 最短增广路算法模板(SAP)
- 内置函数、匿名函数,递归函数
- mysql 5.6 gtid 主从_MySQL5.6基于GTID的主从复制
- java listnode 合并链表_剑指offer:合并两个排序的链表(Java)
- SpringBoot之Thymeleaf
- 《Python编程快速上手》8.9 实践项目
- [Linux] 通过shell给unix socket发送数据
- 【一文读懂】Contours Hierarchy ——opencv边界的继承结构,表格的提取,表格孔洞处理,空心形状结构的提取
- 计算机是学前端开发好还是后端开发好?
- C# GDI winfrom 图像转换椭圆形
- 计算机主机只有一块硬盘,电脑双硬盘只显示一个怎么办
- Python也有对象了哈哈哈哈哈哈嗝
- 软件中存在的技术风险
- js中的_poto_和prototype的问题
- 汇编SHR、SHL、SAR、SAL、ROL、ROR、RCL、RCR指令
- 超级高铁原型机现身迪拜,时速高达1200公里!
- 【课后习题】 线性代数第六版第一章 行列式 习题一