上一篇文章,我们介绍了gtest环境搭建,传送门:(87条消息) 【CMake】gtest环境搭建与TDD入门(二)_bluebonnet27的博客-CSDN博客
这次,我们来介绍一下TDD(测试驱动开发)原则。当然,用的还是上一节的例子,要解决的问题也比较简单。
前言
什么是TDD
TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。
简单来说,测试驱动开发。
好处和坏处我就不提了,很明显,TDD可以写出更稳定更安全的代码,但代价就是更长的编码时间和更繁琐的代码。
如何TDD
贴一张图:
这图在网上能找到很多,它揭示了TDD的三个基本步骤:
- 写一个测试用例。这个测试用例通常会失败。
- 写刚好能让测试用例通过的实现。此处不要写多余的代码。
- 重构代码。之后接着写测试用例,即1。
三个基本步骤对应TDD的三原则:
- 除非是为了使一个失败的 unit test 通过,否则不允许编写任何产品代码
- 在一个单元测试中,只允许编写刚好能够导致失败的内容(编译错误也算失败)
- 只允许编写刚好能够使一个失败的 unit test 通过的产品代码
事实上,上面的步骤简化了很多,我后面举的例子更简化。这篇文章的重点是领会TDD的思想。各个大公司对TDD有各自的实践,具体开发的时候还是应该跟自己团队的节奏走。
准备
如果你没有看过上一篇文章,我在这里将题目再给出来。这是LeetCode的 1154题:
给你一个按 YYYY-MM-DD 格式表示日期的字符串 date,请你计算并返回该日期是当年的第几天。
通常情况下,我们认为 1 月 1 日是每年的第 1 天,1 月 2 日是每年的第 2
天,依此类推。每个月的天数与现行公元纪年法(格里高利历)一致。
为了简化开发,我们规定这一功能由以下函数实现:
int calculateDate(int year,int month,int day);
接下来需要划分需求,我也是临时开写,所以简单划分如下:
测试1
datet.cpp
如下:
#include "datet.h"
int calculateDate(int year,int month,int day){
return 1;
}
datet_unittest.cpp
如下:
#include "gtest/gtest.h"
#include "datet.h"
TEST(dayFirstTest,dayFirst){
EXPECT_EQ(calculateDate(2022,1,1),1);
}
int main(int argc, char **argv) {
testing::InitGoogleTest(&argc,argv);
return RUN_ALL_TESTS();
}
这是我们的第一个测试用例,看看结果:
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from dayFirstTest
[ RUN ] dayFirstTest.dayFirst
[ OK ] dayFirstTest.dayFirst (1 ms)
[----------] 1 test from dayFirstTest (7 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (26 ms total)
[ PASSED ] 1 test.
通过了,很棒。
接下来的代码,我会省略无关的部分,只展示核心代码。
TDD Step1 :编写测试用例
接下来编写一个新的测试用例:
TEST(monthZeroTest,day_2){
EXPECT_EQ(calculateDate(2022,1,2),2);
}
显然,无法通过,结果也验证了我的看法:
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from monthZeroTest
[ RUN ] monthZeroTest.day_1
[ OK ] monthZeroTest.day_1 (0 ms)
[ RUN ] monthZeroTest.day_2
D:\Projects\vscodeProjectSpace\DateTestMy\test\datet_unittest.cpp:9: Failure
Expected equality of these values:
calculateDate(2022,1,2)
Which is: 1
2
[ FAILED ] monthZeroTest.day_2 (16 ms)
[----------] 2 tests from monthZeroTest (27 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (48 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] monthZeroTest.day_2
gtest给出了不通过的具体信息,1月2日应该是第2天,但我们返回了1.
TDD Step2:编写刚好让测试用例通过的代码
修改代码如下:
int calculateDate(int year,int month,int day){
return day;
}
重新测试,这次就通过了:
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from monthZeroTest
[ RUN ] monthZeroTest.day_1
[ OK ] monthZeroTest.day_1 (0 ms)
[ RUN ] monthZeroTest.day_2
[ OK ] monthZeroTest.day_2 (0 ms)
[----------] 2 tests from monthZeroTest (12 ms total)
[----------] Global test environment tear-down
[==========] 2 tests from 1 test suite ran. (32 ms total)
[ PASSED ] 2 tests.
TDD Step3:重构
我们的代码只有一行,似乎没什么可重构的。
测试2
接下来的步骤,对于通过的测试,我将只告知结果,只展示未通过的测试的失败的gtest输出。
TDD Step1 :编写测试用例
新的测试用例如下:
TEST(monthOneTest,day_1){
EXPECT_EQ(calculateDate(2022,2,1),32);
}
测试失败,部分测试结果如下:
[----------] 1 test from monthOneTest
[ RUN ] monthOneTest.day_1
D:\Projects\vscodeProjectSpace\DateTestMy\test\datet_unittest.cpp:13: Failure
Expected equality of these values:
calculateDate(2022,2,1)
Which is: 1
32
[ FAILED ] monthOneTest.day_1 (13 ms)
[----------] 1 test from monthOneTest (18 ms total)
TDD Step2:编写刚好让测试用例通过的代码
问题在于日期发生了改变,因此我们将1月的日期加进来就好了。
int calculateDate(int year,int month,int day){
return day + 31;
}
这样通过了第二次的测试,但没有通过第一次的,
D:\Projects\vscodeProjectSpace\DateTestMy\test\datet_unittest.cpp:5: Failure
Expected equality of these values:
calculateDate(2022,1,1)
Which is: 32
1
[ FAILED ] monthZeroTest.day_1 (15 ms)
[ RUN ] monthZeroTest.day_2
D:\Projects\vscodeProjectSpace\DateTestMy\test\datet_unittest.cpp:9: Failure
Expected equality of these values:
calculateDate(2022,1,2)
Which is: 33
2
[ FAILED ] monthZeroTest.day_2 (14 ms)
[----------] 2 tests from monthZeroTest (41 ms total)
因此还需要修改代码,修改后的如下:
int calculateDate(int year,int month,int day){
if (month == 2)
{
return day + 31;
}
return day;
}
这样通过了目前的测试。
TDD Step3:重构
依然没什么需要重构的。
测试3
TDD Step1 :编写测试用例
新的测试用例如下:
TEST(monthTwoTest,day_2){
EXPECT_EQ(calculateDate(2022,3,2),61);
}
没有通过。
TDD Step2:编写刚好让测试用例通过的代码
需要再加一个else-if条件,这样保证前面的测试能通过,现在的测试也能通过:
int calculateDate(int year,int month,int day){
if (month == 2)
{
return day + 31;
}else if (month == 3)
{
return day + 31 + 28;
}
return day;
}
这样,4个测试用例就全通过了。
TDD Step3:重构
结构上似乎没什么需要重构的,但魔法数字太多了,用const定义消去一些魔法数字:
const int feb = 2;
const int mar = 3;
const int day_jan = 31;
const int day_feb = 28;
int calculateDate(int year,int month,int day){
if (month == feb)
{
return day + day_jan;
}else if (month == mar)
{
return day + day_jan + day_feb;
}
return day;
}
这涉及到一些有关cleancode的知识,我可能会在以后的文章给出。
测试4
TDD Step1 :编写测试用例
新的测试用例如下:
TEST(monthThreeTest,day_2){
EXPECT_EQ(calculateDate(2022,4,2),92);
}
没有通过测试。
题外话,你可能觉得,很多地方是显而易见的。主要是我们举的例子太简单了,不使用TDD完成可能会更快一点。但是:
- 事实上需要TDD的大型项目必然不会这么简单
- TDD有助于编写完备的测试用例,而一些公司(比如我目前任职的)对测试用例的完备性有很大的要求。
TDD Step2:编写刚好让测试用例通过的代码
加条件就好了:
int calculateDate(int year,int month,int day){
if (month == feb)
{
return day + day_jan;
}else if (month == mar)
{
return day + day_jan + day_feb;
}else if (month == apr)
{
return day + day_jan + day_feb + day_mar;
}
return day;
}
所有测试全部通过。
TDD Step3:重构
观察代码,多个else-if的结构并不合理。显然,月份天数的计算存在一定的累计关系,我们使用for循环代替简单的else-if嵌套,同时,为31天、28天的天数变量单独取名字,取代之前每个月的天数变量:
int calculateDate(int year,int month,int day){
int days = day;
for (int i = 1; i < month; i++)
{
if(i == jan || i == mar)
{
days += day_solar;
}else if (i == feb)
{
days += day_feb_lunar;
}
}
return days;
}
由于我们的计数器最多只到3月,事实上5月的测试用例,这个代码也是无法应对的。
测试5
TDD Step1 :编写测试用例
让日期到5月吧,新的测试用例如下:
TEST(monthFourTest,day_11){
EXPECT_EQ(calculateDate(2022,5,11),131);
}
TDD Step2:编写刚好让测试用例通过的代码
需要把5月的情况考虑在内:
int calculateDate(int year,int month,int day){
int days = day;
for (int i = 1; i < month; i++)
{
if(i == jan || i == mar)
{
days += day_solar;
}else if (i == apr)
{
days += day_lunar;
}else if (i == feb)
{
days += day_feb_lunar;
}
}
return days;
}
TDD Step3:重构
月份的变量名并不合理,更改一部分变量名字:
int calculateDate(int year,int month,int day){
int days = day;
for (int i = 1; i < month; i++)
{
if(i == jan || i == mar)
{
days += day_month_solar;
}else if (i == apr)
{
days += day_month_lunar;
}else if (i == feb)
{
days += day_month_feb_lunar;
}
}
return days;
}
结尾
按照这个原则继续向下编写代码,最终问题就会按照TDD的原则编码解决。剩下的代码,以及有关TDD的更多的内容会在下一篇文章讨论。