Java TDD介绍-1

原文:Introduction in Java TDD – part 1
翻译:get-set


欢迎来到关于测试驱动开发(TDD)的简介。我们会谈到关于Java和JUnit在TDD中的应用,不过这些都只是工具而已。这篇文章的主要目的是能给您一个关于TDD的深入的理解,而不管什么编程语言和测试框架。

如果你不在你的项目中使用TDD,那么说明你要么懒,要么就是不懂TDD,缺少时间这种理由是站不住脚的。

关于这篇文章

在这篇文章中,我会解释什么是TDD,以及如何在Java中使用;在TDD的什么地方会用到单元测试;在单元测试中你需要做哪些事情;最后,你应该遵循什么原则来编写完美有效的单元测试代码。
如果你已经知道关于Java中的TDD,但是又对例子和教程比较感兴趣,建议你跳过本节直接阅读下一节。

什么是TDD?

如果有人让我简单解释一下什么是TDD,我会说TDD就是在你还没有实现一项功能之前就进行的测试。你可能会质疑:如果还没有实现那如何测试呢?估计肯特·贝克(估计你也猜到了他就是TDD的创始人)会赏你一巴掌。

那么具体如何来做呢?大概有如下几步:
1. 阅读并理解功能需求;
2. 开发一系列的测试来检查这项功能。因为还没有实现功能,因此所有的测试都是红色的;
3. 开发这项功能,直到所有的测试都变成绿色的;
4. 重构代码。
这里写图片描述
TDD需要的是不同的思路,所以为了基于TDD的理念开始开发,你需要首先忘掉之前的开发思路。这个过程非常痛苦,而且如果你不懂如何编写单元测试的话会更加痛苦,但却是值得的。

基于TDD开发有很大的优势:
1. 你对要实现的功能会有一个更好的理解;
2. 你有明确的“功能是否完成”的指标;
3. 基于测试开发的代码通常之后不需要更多的修改和完善。

得到这一优势的代价也很大——调整到一个新的开发模式的时候的阵痛,以及你在开发每一个新功能的时候可能会花费更多的时间。不过这是时间换质量的代价。

总之这就是TDD的工作原理——编写红色的单元测试,实现相关功能,让所有的测试变绿,重构。

TDD中单元测试的时机

单元测试是自动化测试金字塔中最小的元素,TDD也是基于此。单元测试可以帮助我们检查业务逻辑,编写单元测试代码也很容易。那么你如何使用单元测试呢?我会尽量通过简单的方式解释给你。
这里写图片描述
单元测试应该越小越好。但是不要认为一个方法对应一个测试,不过也有可能存在这种情况。真正的规则在于一个单元测试对应一组多个方法的调用,这叫做“基于行为的测试”(testing of behaviour)。
我们来看一下Account这个类:

public class Account {

    private String id = RandomStringUtils.randomAlphanumeric(6);
    private boolean status;
    private String zone;
    private BigDecimal amount;

    public Account() {
        status = true;
        zone = Zone.ZONE_1.name();
        amount = createBigDecimal(0.00);
    }

    public Account(boolean status, Zone zone, double amount) {
        this.status = status;
        this.zone = zone.name();
        this.amount = createBigDecimal(amount);
    }

    public enum Zone {
        ZONE_1, ZONE_2, ZONE_3
    }

    public static BigDecimal createBigDecimal(double total) {
        return new BigDecimal(total).setScale(2, BigDecimal.ROUND_HALF_UP);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("id: ").append(getId())
                .append("\nstatus: ")
                .append(getStatus())
                .append("\nzone: ")
                .append(getZone())
                .append("\namount: ")
                .append(getAmount());
        return sb.toString();
    }

    public String getId() {
        return id;
    }

    public boolean getStatus() {
        return status;
    }

    public void setStatus(boolean status) {
        this.status = status;
    }

    public String getZone() {
        return zone;
    }

    public void setZone(String zone) {
        this.zone = zone;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public void setAmount(BigDecimal amount) {
        if (amount.signum() < 0)
            throw new IllegalArgumentException("The amount does not accept negative values");
        this.amount = amount;
    }
}

注意,这个类中有4个getter方法。如果我们为每个getter方法创建一个单独的单元测试,代码就会有很多冗余行。这种情况下基于行为的测试就比较有用了。试想我们现在需要测试某一个构造器在创建对象时是否正确,如何检查创建的对象是否如预期呢?我们需要检查每一个属性的值,getter方法就可以用于这样的场景。

创建小而精的单元测试,因为它们需要在每次提交到git或编译之前都应该执行。为了理解单元测试的速度的重要性,试想一个项目有1000个单元测试,每个测试花费100ms,那么跑完所有的测试将花费1分40秒的时间。
实际上对于单元测试来说,100ms的时间太长了,所以你应当通过采用不同的规则和技术来降低测试时间,比如,不要在单元测试中进行数据库连接的测试,也不要在@Before注解的地方运行大型对象的初始化。

要给单元测试起好名字。测试的名字可以很长,主要是它能够表达清楚测试什么。例如我需要测试Account类的一个构造方法,我会命名为defaultConstructorTest。另一方面,建议在命名之前先把测试逻辑写出来,这样可能会更容易命名。

单元测试应该是可预测的。这是最明显不过的了。例如,为了检查转账(5%手续费)操作,你需要知道转账的金额和作为输出的到账金额。比如转账100块而收到95块。

最后,单元测试结果是容易获得的。当你给每一个业务逻辑场景都构建一个测试的时候,可以得到很多测试结果反馈,从而能够精确定位到测试失败的场景。

所有的这些建议目的在于提高单元测试的设计水平,但是还有一点——基本的测试设计原理。

基本的测试设计原理

如果没有测试数据,那么测试也无从谈起。例如,当你测试转账业务逻辑时,你需要设置一些数字作为转账发起金额。这些金额就是测试数据。那么问题来了,你应该如何选择测试数据呢?为了回答这个问题,我们需要过一遍最主流的测试设计原理。其主要目的就是构建测试数据。

首先,我们假设转账金额只能为正整数,另外上限是1000元。那么可以表示为:

0 < amount <= 1000; amount in integer

我们所有的测试场景可以被分为两组:正确 & 错误。第一组测试数据是允许的,应该得到正确的结果;第二组就是错误场景。

依据同类数据原理(classes of equivalence technic),我们随机选取(0, 1000]范围内的数字进行测试,比如500,如果500成功的话,我们认为所有的位于该范围内的数字也是成功的。我们也可以选择区域内的其他类型数据,比如浮点型数字123.45。

然后我们依据边界测试原理(boundary testing technic),选择2个有效值,分别位于数字范围的两头,这里我们选择1作为最小值,1000作为最大值。

下一步就是选择2个无效值,比如0和1001.

最终我们有6个测试数据:
(1, 500, 1000)——作为正确场景
(0, 125.50, 1001)——作为错误场景

总结

在这篇文章中,我解释了TDD以及单元测试在TDD中的重要性。我希望在如此啰嗦的解释后,大家能够在实际的开发过程中进行应用。下一节我将会说明如何在功能代码开发前编写测试代码。我们会一步一步来,以一个文件分析的例子开始,直到最终完成代码重构。

一定让所有的测试都变成绿色的哟 : )

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值