如何优雅的写java单元测试

作为一名Java开发人员,对于单元测试你或多或少都是要接触的。那么我们如何的优雅的去写Java单元测试呢?下面我们一起来了解一下关于java单元测试的基础教程。

如何优雅的写java单元测试

前言

前面在讲什么是单元测试的时候,我们举了一个给 toNumber () 函数写单元测试的例子。根据那个例子,我们可以总结得出,写单元测试就是针对代码设计覆盖各种输入、异常、边界条件的测试用例,并将这些测试用例翻译成代码的过程。

在把测试用例翻译成代码的时候,我们可以利用单元测试框架,来简化测试代码的编写。比如,Java 中比较出名的单元测试框架有 Junit、TestNG、Spring Test 等。这些框架提供了通用的执行流程(比如执行测试用例的 TestCaseRunner)和工具类库(比如各种 Assert 判断函数)等。借助它们,我们在编写测试代码的时候,只需要关注测试用例本身的编写即可。

针对 toNumber () 函数的测试用例,我们利用 Junit 单元测试框架重新实现一下,具体代码如下所示。你可以拿它跟之前没有利用测试框架的实现方式对比一下,看是否简化了很多呢?

import org.junit.Assert;

import org.junit.Test;

public class TextTest {

  @Test

  public void testToNumber() {

    Text text = new Text("123");

    Assert.assertEquals(new Integer(123), text.toNumber());

  }

  @Test

  public void testToNumber_nullorEmpty() {

    Text text1 = new Text(null);

    Assert.assertNull(text1.toNumber());

    Text text2 = new Text("");

    Assert.assertNull(text2.toNumber());

  }

  @Test

  public void testToNumber_containsLeadingAndTrailingSpaces() {

    Text text1 = new Text(" 123");

    Assert.assertEquals(new Integer(123), text1.toNumber());

    Text text2 = new Text("123 ");

    Assert.assertEquals(new Integer(123), text2.toNumber());

    Text text3 = new Text(" 123 ");

    Assert.assertEquals(new Integer(123), text3.toNumber());

  }

  @Test

  public void testToNumber_containsMultiLeadingAndTrailingSpaces() {

    Text text1 = new Text("  123");

    Assert.assertEquals(new Integer(123), text1.toNumber());

    Text text2 = new Text("123  ");

    Assert.assertEquals(new Integer(123), text2.toNumber());

    Text text3 = new Text("  123  ");

    Assert.assertEquals(new Integer(123), text3.toNumber());

  }

  @Test

  public void testToNumber_containsInvalidCharaters() {

    Text text1 = new Text("123a4");

    Assert.assertNull(text1.toNumber());

    Text text2 = new Text("123 4");

    Assert.assertNull(text2.toNumber());

  }

}

对于如何使用这些单元测试框架,大部分框架都给出了非常详细的官方文档,你可以自行查阅。这些东西理解和掌握起来没有太大难度,所以这不是要讲解的重点。关于如何编写单元测试,我更希望传达给你一些我的经验总结。具体包括以下几点。

1. 写单元测试真的是件很耗时的事情吗?

尽管单元测试的代码量可能是被测代码本身的 1~2 倍,写的过程很繁琐,但并不是很耗时。毕竟我们不需要考虑太多代码设计上的问题,测试代码实现起来也比较简单。不同测试用例之间的代码差别可能并不是很大,简单 copy-paste 改改就行。

2. 对单元测试的代码质量有什么要求吗?

单元测试毕竟不会在产线上运行,而且每个类的测试代码也比较独立,基本不互相依赖。所以,相对于被测代码,我们对单元测试代码的质量可以放低一些要求。命名稍微有些不规范,代码稍微有些重复,也都是没有问题的。

3. 单元测试只要覆盖率高就够了吗?

单元测试覆盖率是比较容易量化的指标,常常作为单元测试写得好坏的评判标准。有很多现成的工具专门用来做覆盖率统计,比如,JaCoCo、Cobertura、Emma、Clover。覆盖率的计算方式有很多种,比较简单的是语句覆盖,稍微高级点的有:条件覆盖、判定覆盖、路径覆盖。

不管覆盖率的计算方式如何高级,将覆盖率作为衡量单元测试质量的唯一标准是不合理的。实际上,更重要的是要看测试用例是否覆盖了所有可能的情况,特别是一些 corner case。我来举个简单的例子解释一下。

像下面这段代码,我们只需要一个测试用例就可以做到 100% 覆盖率,比如 cal (10.0, 2.0),但并不代表测试足够全面了,我们还需要考虑,当除数等于0的情况下,代码执行是否符合预期。

public double cal(double a, double b) {

if (b != 0) {

return a / b;

}

}
实际上,过度关注单元测试的覆盖率会导致开发人员为了提高覆盖率,写很多没有必要的测试代码,比如 get、set 方法非常简单,没有必要测试。从过往的经验上来讲,一个项目的单元测试覆盖率在 60~70% 即可上线。如果项目对代码质量要求比较高,可以适当提高单元测试覆盖率的要求。

4. 写单元测试需要了解代码的实现逻辑吗?

单元测试不要依赖被测试函数的具体实现逻辑,它只关心被测函数实现了什么功能。我们切不可为了追求覆盖率,逐行阅读代码,然后针对实现逻辑编写单元测试。否则,一旦对代码进行重构,在代码的外部行为不变的情况下,对代码的实现逻辑进行了修改,那原本的单元测试都会运行失败,也就起不到为重构保驾护航的作用了,也违背了我们写单元测试的初衷。

5. 如何选择单元测试框架?

写单元测试本身不需要太复杂的技术,大部分单元测试框架都能满足。在公司内部,起码团队内部需要统一单元测试框架。如果自己写的代码用已经选定的单元测试框架无法测试,那多半是代码写得不够好,代码的可测试性不够好。这个时候,我们要重构自己的代码,让其更容易测试,而不是去找另一个更加高级的单元测试框架。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

关注我们