01、前言

很早之前,曾在网络上见到过 TDD 这 3 个大写的英文字母,它是 Test Driven Development 这三个单词的缩写,也就是“测试驱动开发”的意思——听起来很不错的一种理念。

其理念主要是确保两件事:

  • 确保所有的需求都能被照顾到。
  • 在代码不断增加和重构的过程中,可以检查所有的功能是否正确。

但后来很长一段时间里,都没再听过 TDD 的消息。有人说,TDD 已经死了,给出的意见如下:

1)通常来说,开发人员不应该在没有失败的测试用例下编写代码——这似乎是合理的,但是它可能导致过度测试。例如,为了保证一行生产代码的正确性,你不由得写了 4 行测试代码,这意味着一旦这一行生产代码需要修改,你也得修改那 4 行测试代码。

2)为了遵循 TDD 而写的代码,容易进入一个误区:代码是为了满足测试用的,而忽略了实际需求。

02、TDD 到底是什么?

不管 TDD 到底死了没有,先让我们来回顾一下 TDD 到底是什么。

TDD 的基本思想就是在开发功能代码之前,先编写测试代码。也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试代码的编写,然后编写相关的代码满足这些测试用例。然后循环进行添加其他功能,直到完成全部功能的开发。

TDD 的基本过程可以拆解为以下 6 个步骤:

1) 分析需求,把需求拆分为具体的任务。

2) 从任务列表中取出一个任务,并对其编写测试用例。

3) 由于没有实际的功能代码,测试代码不大可能会通过(红)。

4) 编写对应的功能代码,尽快让测试代码通过(绿)。

5) 对代码进行重构,并保证测试通过(重构)。

6) 重复以上步骤。

可以用下图来表示上述过程。

03、TDD 的实践过程

通常情况下,我们都习惯在需求分析完成之后,尽快地投入功能代码的编写工作中,之后再去调用和测试。

而 TDD 则不同,它假设我们已经有了一个“测试用户”了,它是功能代码的第一个使用者,尽管功能代码还不太完善。

当我们站在“测试用户”的角度去写测试代码的时候,我们要考虑的是,这个“测试用户”该如何使用功能代码呢?是通过一个类直接调用方法呢(静态方法),还是构建类的实例去调用方法呢(实例方法)?这个方法如何传参呢?方法如何命名呢?方法有返回值吗?

有了测试代码后,我们开始编写功能代码,并且要以最快地速度让测试由“红”变为“绿”,可能此时的功能代码很不优雅,不过没关系。

当测试通过以后,我们就可以放心大胆的对功能代码进行“重构”了——优化原来比较丑陋、臃肿、性能偏差的代码。

接下来,假设我们接到了一个开发需求:

汪汪队要到小镇冒险岛进行表演,门票为 99 元,冒险岛上唯一的一个程序员王二需要开发一款可以计算门票收入的小程序。

按照 TDD 的流程,王二需要先使用 Junit 编写一个简单的测试用例,测试预期是:销售一张门票的收入是 99 元。

public class TicketTest {

    private Ticket ticket;

    @Before
    public void setUp() throws Exception {
        ticket = new Ticket();
    }     @Test
    public void test() {
        BigDecimal total = new BigDecimal("99");         assertEquals(total, ticket.sale(1));
    } }

为了便于编译能够顺利通过,王二需要一个简单的 Ticket 类:

public class Ticket {

    public BigDecimal sale(int count) {
        return BigDecimal.ZERO;
    } }

测试用例运行结果如下图所示,红色表示测试没有通过:预期结果是 99,实际结果是 0。

那接下来,王二需要快速让测试通过,Ticket.sale() 方法修改后的结果如下:

public class Ticket {

    public BigDecimal sale(int count) {
        if (count == 1) {
            return new BigDecimal("99");
        }
        return BigDecimal.ZERO;
    } }

再运行一下测试用例,结果如下图所示,绿色表示测试通过了:预期结果是 99,实际结果是 99。

绿了,绿了,测试通过了,到了该重构功能代码的时候了。99 元是个魔法数字,至少应该声明成常量,对吧?

public class Ticket {
    private final static int PRICE = 99;     public BigDecimal sale(int count) {
        if (count == 1) {
            return new BigDecimal(PRICE);
        }
        return BigDecimal.ZERO;
    } }

重构完后再运行一下测试用例,确保测试通过的情况下,再增加几个测试用例,比如说门票销量为负数、零甚至一千的情况。

public class TicketTest {

    private Ticket ticket;

    @Before
    public void setUp() throws Exception {
        ticket = new Ticket();
    }     @Test
    public void testOne() {
        BigDecimal total = new BigDecimal("99");         assertEquals(total, ticket.sale(1));
    }     @Test(expected=IllegalArgumentException.class)
    public void testNegative() {
        ticket.sale(-1);
    }     @Test
    public void testZero() {
        assertEquals(BigDecimal.ZERO, ticket.sale(0));
    }     @Test
    public void test1000() {
        assertEquals(new BigDecimal(99000), ticket.sale(1000));
    } }

销量为负数的时候,王二希望功能代码能够抛出异常;销量为零的时候,功能代码的计算结果应该为零;销量为一千的时候,计算结果应该为 99000。

重新运行一下测试用例,结果如下图所示:

有两个测试用例没有通过,那么王二需要继续修改功能代码,调整如下:

public class Ticket {
    private final static int PRICE = 99;     public BigDecimal sale(int count) {
        if (count < 0) {
            throw new IllegalArgumentException("销量不能为负数");
        }         if (count == 0) {
            return BigDecimal.ZERO;
        }         if (count == 1) {
            return new BigDecimal(PRICE);
        }         return new BigDecimal(PRICE * count);
    } }

再运行一下测试用例,发现都通过了。又到了重构的时候了,销量为零、或者大于等于一的时候,代码可以合并,于是重构结果如下:

public class Ticket {
    private final static int PRICE = 99;     public BigDecimal sale(int count) {
        if (count < 0) {
            throw new IllegalArgumentException("销量不能为负数");
        }         return new BigDecimal(PRICE * count);
    } }

重构结束后,再运行测试用例,确保重构后的代码依然可用。

04、最后

从上面的实践过程可以得出如下结论:

TDD 想要做的就是让我们对自己的代码充满信心,因为我们可以通过测试代码来判断这段代码是否正确无误。

也就是说,TDD 流程比较关键的一环在于如何写出有效的测试代码,这里有 4 个原则可以参考:

1)测试过程应该尽量模拟正常使用的过程。

2)应该尽量做到分支覆盖。

3)测试数据应该尽量包括真实数据,以及边界数据。

4)测试语句和测试数据应该尽量简单,容易理解。

注意,这 4 个原则不仅适用于 TDD,同样适用于任何流程下的单元测试。

最后,我想说的是,不管 TDD 有没有死,TDD 都不是银弹,不可能适合所有的场景,但这不应该成为我们拒绝它的理由。

TDD(测试驱动开发)死了吗?的更多相关文章

  1. TDD(测试驱动开发)培训录

    2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都是复杂问题,改变人,改变一个组 ...

  2. TDD(测试驱动开发)培训录(转)

    本文转载自:http://www.cnblogs.com/whitewolf/p/4205761.html 最近也在了解TDD,发现这篇文章不错,特此转载一下. TDD(测试驱动开发)培训录 2015 ...

  3. TDD(测试驱动开发)学习一:初识TDD

    首先说一下名词解释,TDD,英文名称Test-Driven Development,中文名称测试驱动开发,简单的断下句“测试/驱动/开发”,简单的理解一下,就是测试驱动着开发,大白话就是说用一边测试一 ...

  4. TDD(测试驱动开发)

    TDD(测试驱动开发)培训录 2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都 ...

  5. TDD(测试驱动开发)学习二:创建第一个TDD程序

    本节我们将学习一些测试驱动开发环境的搭建,测试驱动开发概念和流程.所涉及的内容全部会以截图的形式贴出来,如果你也感兴趣,可以一步一步的跟着来做,如果你有任何问题,可以进行留言,我也会很高兴的为你答疑. ...

  6. (译)TDD(测试驱动开发)的5个步骤

    原文:5 steps of test-driven development https://developer.ibm.com/articles/5-steps-of-test-driven-deve ...

  7. 基于SOA架构的TDD测试驱动开发模式

    以需求用例为基,Case&Coding两条线并行,服务(M)&消费(VC)分离,单元.接口.功能.集成四层质量管理,自动化集成.测试.交付全程支持. 3个大阶段(需求分析阶段.研发准备 ...

  8. TDD(测试驱动开发)的推广方法论

  9. 测试驱动开发(TDD)的思考

    极限编程 敏捷开发是一种思想,极限编程也是一种思想,它与敏捷开发某些目标是一致的.只是实现方式不同.测试驱动开发是极限编程的一部分. 1.极限编程这个思路的来源 Kent Beck先生最早在其极限编程 ...

  10. 测试驱动开发 TDD

    一.详解TDD 1.1.TDD概念 :Test Drived Develop 测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种方法论.TDD的原理是在开发功能代码之前,编写单元测试用例代码,测试 ...

随机推荐

  1. 乞讨 间隔[a,b]在见面p^k*q*^m(k&gt;m)中数号码

    标题叙述性说明: 1<=a,b<=10^18,p,q他们是素数  2<=p,q<=10^9; 求在[a,b]内能够表示为  x*p^k*q^m  k > m   的数的个 ...

  2. Win10局域网内无法共享的解决方法分享(开启Server和ComputerBrowser服务,其它文章也不错)

    局域网共享是办公环境下经常使用的,而有些用户反应在升级到win10系统后,网络总是无法共享,给用户带来了很大的困扰,如果你也遇上这样的情况,并通过一些方法无较后,不妨尝试下小编提供的方法. 如果在Wi ...

  3. sklearn、theano、TensorFlow 以及 theras 的理解

    sklearn ⇒ 机器学习算法和模型: theras theano TensorFlow 1. 理解模型以及函数,参数返回值的实际意义 一定要注意模型的构造函数,接收的参数列表,以及该模型本身所要解 ...

  4. 从hadoop 要删除字符串匹配指定的任务

    我们都知道,假设 hadoop job -list 获取当前正在执行的hadoop 任务,返回的结果例如以下: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQ ...

  5. python中string的操作函数

    在python有各种各样的string操作函数.在历史上string类在python中经历了一段轮回的历史.在最开始的时候,python有一个专门的string的module,要使用string的方法 ...

  6. Android在putString和getString使用方法

    函数: putString(String key,String value); 功能:将键为key的值为value. 详细的容器详细考虑比如对于Editor 的对象来讲: 代码例如以下: 首先新建一个 ...

  7. img前置显示屏装load图片

    只需要设置img的background能够 <img src="" alt="" class="detailImg"  > cs ...

  8. TypeScript Array Remove

    定义一个数组 tags:any[]; 方法1:filter 删除并不留空位 this.tags = this.tags.filter(tag => tag !== removedTag); 方法 ...

  9. 【C#】解决MouseHook捕获鼠标动作,在有些电脑上SetWindowsHookEx失败返回0的问题

    原文:[C#]解决MouseHook捕获鼠标动作,在有些电脑上SetWindowsHookEx失败返回0的问题 最近在debug鼠标位置捕获的功能时发现在其中的一台开发电脑上,SetWindowsHo ...

  10. ELINK离线编程器常见问题

    Q1 编程器是否可以接JTAG JTAG接口已经包含SWD接口引脚,按以下引脚对应接线即可: SWDIO->目标板JTAG 的JTMS SWCLK->目标板JTAG 的JTCK Q2 PC ...