测试驱动开发之初窥门径

什么是测试驱动开发

测试驱动开发是指在编写实现代码之前先写测试代码的开发方式。JUnit的作者Kent Beck说过:编写测试驱动代码的重要原因是消除开发中的恐惧和不确定性,因为编写代码时的恐惧会让你小心试探,让你回避沟通,让你羞于得到反馈,让你变得焦躁不安,而TDD是消除恐惧、让Java开发者更加自信更加乐于沟通的重要手段。TDD会带来的好处可能不会马上呈现,但是你在某个时候一定会发现,这些好处包括:

  1. 更清晰的代码 — 只写需要的代码
  2. 更好的设计
  3. 更出色的灵活性 — 鼓励程序员面向接口编程
  4. 更快速的反馈 — 不会到系统上线时才知道bug的存在

TDD可以在多个测试级别上使用,如下表所示:

测试级别 描述
单元测试 测试类中的代码
集成测试 测试类之间的交互
系统测试 测试运行中的系统
系统集成测试 测试运行中的系统包括第三方组件

测试驱动开发的例子

现在我们需要一段代码来计算某个电影放映厅的门票收入,当前的业务规则非常简单,包括:

  • 每张票售价(单价)¥30
  • 收入=门票销售数量*单价
  • 放映厅最多容纳100人

这里还有一个假设:目前因为没有专业的设备或系统来统计门票销售的数量,在计算门票收入时,门票销售数量是由使用者手动录入的。
TDD的基本步骤是:红色-绿色-重构。

  1. 红色 - 编写无法通过的测试
  2. 绿色 - 编写实现代码并尽快让测试可以通过
  3. 重构 - 重构代码并再次让测试通过

接下来我们按照上述步骤完成门票收入计算的功能。

package com.lovo;

import java.math.BigDecimal;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class TicketRevenueTest {
    private TicketRevenue ticketRevenue;
    private BigDecimal expectedRevenue;

    @Before
    public void setUp() {
        ticketRevenue= new TicketRevenue();
    }

    @Test
    public void oneTicketSoldIsThirtyInRevenue() {
        expectedRevenue = new BigDecimal("30");
        Assert.assertEquals(expectedRevenue,ticketRevenue.estimateTotalRevenue(1));
    }
}

上述测试代码不仅不能通过测试,甚至连编译都无法通过,因为TicketRevenue类还不存在呢。接下来我们可以利用IDE的代码修复功能(Eclipse和IntelliJ都有这样的功能)创建出TicketRevenue类以及该类中计算门票收入的estimateTotalRevenue方法。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {

    public BigDecimal estimateTotalRevenue(int i) {
        return BigDecimal.ZERO;
    }

}

现在可以运行你的单元测试用例了,但是由于我们还没有实现真正的业务逻辑,这个测试是不可能通过的,如下图所示。

但是,迄今为止我们已经完成了“红色“这个步骤。接下来我们修改TicketRevenue类的estimateTotalRevenue方法来让测试通过。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {

    public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) {
        BigDecimal totalRevenue = BigDecimal.ZERO;
        if(numberOfTicketsSold == 1) {
            totalRevenue = new BigDecimal(30);
        }
        return totalRevenue;
    }

}

再次运行单元测试,结果如下图所示。

到这里,第二个步骤”绿色“就完成了。
接下来我们开始重构TicketRevenue类的代码。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {
    private final static int TICKET_PRICE = 30;

    public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) {
        BigDecimal totalRevenue = null;
        totalRevenue = new BigDecimal(TICKET_PRICE * numberOfTicketsSold);
        return totalRevenue;
    }

}

重构后的代码可以根据输入的门票销售数量计算出对应的收入,较之之前的硬代码(hard code)它已经前进了一大步,但是很明显它没有考虑到输入小于0或者大于100的情况。因此我们需要更多的测试例来模拟实际工作环境中可能的输入,我们对刚才的测试代码进行了如下改进。

package com.lovo;

import java.math.BigDecimal;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class TicketRevenueTest {
    private TicketRevenue ticketRevenue;
    private BigDecimal expectedRevenue;

    @Before
    public void setUp() {
        ticketRevenue = new TicketRevenue();
    }

    @Test(expected = IllegalArgumentException.class)
    public void failIfLessThanZeroTicketsAreSold() {
        ticketRevenue.estimateTotalRevenue(-1);
    }

    @Test
    public void zeroSalesEqualsZeroRevenue() {
        Assert.assertEquals(BigDecimal.ZERO,ticketRevenue.estimateTotalRevenue(0));
    }

    @Test
    public void oneTicketSoldIsThirtyInRevenue() {
        expectedRevenue = new BigDecimal("30");
        Assert.assertEquals(expectedRevenue,ticketRevenue.estimateTotalRevenue(1));
    }

    @Test
    public void tenTicketsSoldIsThreeHundredInRevenue() {
        expectedRevenue = new BigDecimal("300");
        Assert.assertEquals(expectedRevenue,ticketRevenue.estimateTotalRevenue(10));
    }

    @Test(expected = IllegalArgumentException.class)
    public void failIfMoreThanOneHundredTicketsAreSold() {
        ticketRevenue.estimateTotalRevenue(101);
    }
}

再次运行测试会发现5个测试中有两个无效输入的测试没有通过(销售数量为-1和101的测试),原因很简单,我们的代码中还没有处理无效输入的代码。接下来继续重构我们的代码。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {
    private final static int TICKET_PRICE = 30;

    public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) 
            throws IllegalArgumentException {
        BigDecimal totalRevenue = null;
        if(numberOfTicketsSold < 0) {
            throw new IllegalArgumentException("门票销售数量必须大于等于0");
        }
        else if(numberOfTicketsSold > 100) {
            throw new IllegalArgumentException("门票销售数量必须小于等于100");
        }
        else {
            totalRevenue = new BigDecimal(TICKET_PRICE * numberOfTicketsSold);
        }
        return totalRevenue;
    }

}

再次运行刚才的测试代码,检查一下你的bar是不是绿色的(JUnit的名言是:“Keep your bar green”)。当然,对于有代码洁癖的人来说,上述代码仍然稍显臃肿,没关系,再来一次重构吧。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {
    private final static int TICKET_PRICE = 30;

    public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) 
            throws IllegalArgumentException {
        if(numberOfTicketsSold < 0 || numberOfTicketsSold > 100) {
            throw new IllegalArgumentException("门票销售数量必须在0到100之间");
        }

        return  new BigDecimal(TICKET_PRICE * numberOfTicketsSold);
    }

}

当你完成对代码的修改后,永远都不要忘记再来一次刚才的测试,仍然需要Keep your bar green。
如果我们使用面向对象的编程范式,那么对代码的重构应当遵循面向对象的设计原则。大神Robert Matin将这些原则总结为SOLID原则。

原则 英文 描述
单一职责原则(S) Single Responsibility Principle 每个对象只做自己该做的事情
开闭原则(O) Open-Closed Principle 接受扩展但不接受修改
里氏替换原则(L) Liskov Substitution Principle 可以用子类型替换父类型
接口隔离原则(I) Interface Segregation Principle 接口要小而专
依赖倒转原则(D) Dependency Inversion Principle 依赖接口而不依赖实现

说明:上面的例子来自The Well-Grounded Java Developer一书(中文名《Java程序员修炼之道》),这本书覆盖了Java开发中很多实用的技术以及Java新的语言特性,有兴趣的可以阅读此书,相信你会从中得到很多收获。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结