【CMake】TDD入门(三)

上一篇文章,我们介绍了gtest环境搭建,传送门:(87条消息) 【CMake】gtest环境搭建与TDD入门(二)_bluebonnet27的博客-CSDN博客

这次,我们来介绍一下TDD(测试驱动开发)原则。当然,用的还是上一节的例子,要解决的问题也比较简单。

前言

什么是TDD

TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。

简单来说,测试驱动开发。

好处和坏处我就不提了,很明显,TDD可以写出更稳定更安全的代码,但代价就是更长的编码时间和更繁琐的代码。

如何TDD

贴一张图:
TDD

这图在网上能找到很多,它揭示了TDD的三个基本步骤:

  1. 写一个测试用例。这个测试用例通常会失败。
  2. 写刚好能让测试用例通过的实现。此处不要写多余的代码。
  3. 重构代码。之后接着写测试用例,即1。

三个基本步骤对应TDD的三原则:

  1. 除非是为了使一个失败的 unit test 通过,否则不允许编写任何产品代码
  2. 在一个单元测试中,只允许编写刚好能够导致失败的内容(编译错误也算失败)
  3. 只允许编写刚好能够使一个失败的 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完成可能会更快一点。但是:

  1. 事实上需要TDD的大型项目必然不会这么简单
  2. 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的更多的内容会在下一篇文章讨论。

猜你喜欢

转载自blog.csdn.net/qq_37387199/article/details/127023443
TDD